Skip to content

candy-hermit: Fix string items, scrolling viewport, HelpBar/StatusBar, and 8 other remediations#1167

Merged
detain merged 10 commits into
masterfrom
ai/candy-hermit-fix
Jun 29, 2026
Merged

candy-hermit: Fix string items, scrolling viewport, HelpBar/StatusBar, and 8 other remediations#1167
detain merged 10 commits into
masterfrom
ai/candy-hermit-fix

Conversation

@detain

@detain detain commented Jun 29, 2026

Copy link
Copy Markdown
Owner

Summary

Implements 12 of 15 remediation steps from findings/plan_candy-hermit.md for candy-hermit:

  • Step 1: Coerce string items to Item at every entry point (prevents fatal at runtime)
  • Step 2: Add string-items end-to-end test coverage
  • Step 3: Harden FileHistory::all() against corrupt JSON lines + handle leak
  • Step 4: Remove dead byte-offset accountant from printableText()
  • Step 5: Add exact-SGR snapshot test for substring highlighter
  • Step 6: Clamp backspace() cursor floor to 0 (prevents -1 cursor)
  • Step 7: Fix attachSigwinch() to query real TTY size via Tty::size()
  • Step 8: Decouple setOffset() from auto-show (pure positioning setter)
  • Step 9: itemFormatter now shows item ordinal (e.g. "3. cherry")
  • Step 10: Scrolling viewport keeps cursor visible in tall lists
  • Step 11: Wire HelpBar and StatusBar into View() output
  • Step 12: Sanitize item text to strip embedded newlines (prevents row injection)

Test plan

68 tests, 147 assertions — all pass.

OK (68 tests, 147 assertions)

Notes

  • Steps 13 (Model fake impl), 14 (HelpBar/StatusBar method tests), 15 (optional matchAll() perf) not implemented
  • candy-hermit is a leaf library — no downstream dependency ordering required

detain added 10 commits June 29, 2026 03:20
Coerce raw strings to FilteredItem in constructor and withItems() so
that $item->value() calls never fatal on string inputs.

Adds private coerceItems() helper that wraps non-Item entries as
FilteredItem($i + 1, (string) $entry) and passes Items through
unchanged. selected() now always returns an Item (not null unless
the filtered list is empty).

Mirrors charmbracelet/bubbletea item coercion pattern.
Tests the full string-items path: coercion in ::new(), filter narrowing
via type(), View() rendering, selected() returning valid Item, withItems()
re-coercion, custom itemFormatter receiving string values, and allCount() /
itemCount() counts.

Mirrors charmbracelet/bubbletea string-item integration test pattern.
Wrap the file-read loop in try/finally to guarantee the handle is closed
even when json_decode throws JsonException. Wrap the json_decode call in
its own try/catch so a single malformed line skips rather than aborting
the full read.

Adds testAllSkipsCorruptLine (verifies 2 valid items returned when a
bad JSON line is sandwiched between good lines) and
testAllClosesHandleEvenOnCorruptLines (verifies repeated all() calls
don't exhaust file descriptors).
The anonymous Handler class tracked $byteOffset and $charPositions
across every print/execute/dispatch call, but only $charString (the
accumulated rune string) was ever returned. Remove the dead accounting
fields and simplify all dispatch methods to empty no-ops.

No functional change: printableText() still returns the same rune string.
Existing highlight tests (CJK, emoji, SGR placement) confirm byte-identical
output is preserved.
Tests that View() output for 'banana' filtered by 'an' with setMatchStyle
yellow (\x1b[33m) contains the exact highlighted fragment
\x1b[33man\x1b[0m, confirming correct SGR wrap placement around
the matched run.

Uses assertNotFalse(strpos) rather than assertStringContainsString
because PHPUnit failure output represents non-printable bytes ([1b) by
their escaped form, which can mislead the eye when comparing byte-exact
needles against raw string haystacks.
When backspace reduces the filtered list to empty, the min() clamp could
yield cursor = -1 (min(cursor, 0-1) = min(cursor, -1) = -1 when cursor >= 0).
Wrap with max(0, ...) so an emptied list always yields cursor 0.

Adds testBackspaceCursorNeverNegative: sets a filter that excludes
everything, types to empty result, backspaces, and asserts cursor() === 0.
Replace the stale COLUMNS/LINES env-var size provider with a live
query via SugarCraft\Core\Util\Tty::size(), wrapped in a private
ttySize() helper that falls back to 80x24 on any exception (signal
handlers must not propagate exceptions). Also fixes a pre-existing
bug where STDIN (a resource) was passed to SignalForwarder::attachSigwinchToFd()
which expects an int file descriptor — now casts to (int) STDIN.

Updates the [pattern:sigwinch-...] CALIBER_LEARNINGS.md note to
reflect the Tty::size() provider.

Adds testAttachSigwinchInstallsHandler: when SIGWINCH+pcntl are
available it asserts attachSigwinch() returns true with a callback
set; skips otherwise.
- Removed $clone->isShown = true from setOffset() — it is now a pure position setter
- Updated testFluentSetters to call ->show() explicitly and added assertion that setOffset alone does NOT show
- Added ->show() to 4 HermitRankerTest methods that relied on setOffset auto-show side effect
- All 63 tests pass
…sBar wiring

Step 9: itemFormatter now accepts optional third int param (number)
- Default closure shows ordinal: '3. cherry' not just 'cherry'
- Updated docblock and call sites to pass number()

Step 10: Scrolling viewport in View()
- Compute top offset to keep cursor visible in window
- Center cursor in visible window when possible
- Added testScrollingViewport tests

Step 11: Wire HelpBar and StatusBar into View()
- HelpBar/StatusBar render() output appended after items
- Bars extend the overlay past windowHeight
- Added testHelpBarAndStatusBarRenderInView
- Strip raw newlines/carriage returns from formatted item strings
- Prevents embedded newlines from injecting extra rows in output
- Added testItemWithEmbeddedNewlineDoesNotInjectRows
@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.

@codacy-production

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 3 medium · 11 minor

Alerts:
⚠ 14 issues (≤ 0 issues of at least minor severity)

Results:
14 new issues

Category Results
UnusedCode 3 medium
CodeStyle 11 minor

View in Codacy

🟢 Metrics 12 complexity · 0 duplication

Metric Results
Complexity 12
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@detain detain merged commit d580adb into master Jun 29, 2026
4 of 6 checks passed
@detain detain deleted the ai/candy-hermit-fix branch June 29, 2026 15:29
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