Skip to content

fix: stabilize PR107 tmux spawn, offline buffering, and bun runtime#14

Open
leeroybrun wants to merge 20 commits intomainfrom
slopus/pr/pr107-followup-fixes
Open

fix: stabilize PR107 tmux spawn, offline buffering, and bun runtime#14
leeroybrun wants to merge 20 commits intomainfrom
slopus/pr/pr107-followup-fixes

Conversation

@leeroybrun
Copy link
Owner

@leeroybrun leeroybrun commented Jan 17, 2026

Context / Goal

This PR is a follow-up to slopus/happy-cli#107 (profiles/env expansion + tmux + runtime/offline changes).
The goal is to make tmux spawning correct and deterministic, align the spawn RPC contract with reality, restore reliable offline buffering, and harden logging.

Summary (what this PR does)

  • Fixes tmux env injection (no value mutation), makes empty session selection deterministic, and properly wires TMUX_TMPDIR for tmux client invocations.
  • Restores offline buffering for socket sends (removes PR107 early-return drops).
  • Makes Bun subprocess spawning coherent and consistent across normal spawn and tmux spawn.
  • Updates the spawn RPC typing to accept arbitrary env var maps (what the GUI actually sends).
  • Tightens DEBUG gating/redaction for logging and makes offline stubs safer.

What slopus#107 introduced (and what we’re fixing here)

tmux correctness

TMUX_TMPDIR contract gap

Offline behavior (“don’t send if disconnected”)

Bun runtime support

Provider config objects (ghost overrides / leftovers?)

RPC typing drift

What this series fixes / improves

1) Stop unintended profile application from CLI settings

  • Problem: daemon implicitly fell back to settings.activeProfileId when GUI didn’t provide env vars, meaning profiles could affect sessions even when the GUI profiles feature was disabled or not used by users (in "standard" non-wizard new session modal, when wizard was disabled in settings).
  • Fix: profile env vars are only applied when explicitly provided in SpawnSessionOptions.environmentVariables.

2) Persist profile identity per session (cross-device visibility)

  • Problem: without persistence, a session started with a profile on machine A couldn’t reliably show “which profile” on machine B.
  • Fix: thread profileId?: string | null through spawn → daemon → child env (HAPPY_SESSION_PROFILE_ID) → session metadata (non-secret, used for display/debugging).

3) Align CLI profile schema + env template support

  • Fixes:
    • Accept URL-or-template strings for provider endpoints (e.g. ${VAR} / ${VAR:-default})
    • Accept non-UUID ids for built-in profiles (string min(1))
    • Support ${VAR:=default} expansion (CLI env-template parity)

4) Prevent secret leakage in daemon + RPC logs

  • Problem: logging spawn params/options could leak tokens and env var values into ~/.happy/logs/*.
  • Fix: log only env var keys/counts and token presence, never values.

5) Prevent secret leakage in happy doctor

  • Problem: doctor printed settings.json verbatim, including profile env var values.
  • Fix: redact/mask secret-like fields; keep ${VAR} templates visible; avoid printing legacy caches.

6) Remove unwired / misleading functionality (strict cleanup)

  • Changes:
    • Removed startupBashScript from the CLI profile schema (it was never executed/passed to spawn by the UI and not wired anywhere in the CLI).
    • Removed localEnvironmentVariables from CLI settings (schema bump + migration deletes legacy key).
    • Removed unused helper APIs that relied on the cache (no call sites existed).

7) Tmux: optional session-env persistence, but only for profile-defined env

  • Behavior:
    • The spawned agent always gets needed env vars via tmux window -e KEY=VALUE (spawn-time).
    • If TMUX_UPDATE_ENVIRONMENT is set (truthy), we additionally persist only profile-provided env vars into the tmux session via tmux set-environment.
    • We intentionally do not persist daemon-injected auth/session markers into the tmux session environment.

8) Tmux: added optionnal integration tests to validate the real behavior against real tmux process

Links to the detailled issues in slopus/happy#107 (links go to the PR diff)

  1. tmux new-window -e env injection corrupts values (quotes/backslashes become literal)
  1. tmux $TMUX parsing is wrong (treats server pid as “session.window”)
  1. tmux “empty session name” selection is “first session from list-sessions” (nondeterministic)
  1. tmux command string built via args.join(' ') (breaks quoting/spacing)
  1. tmux runner treats exitCode=null (killed by signal) as success
  1. TMUX_TMPDIR “contract gap”: profile mapping sets it, but tmux client selection is only via -S socketPath
  1. Daemon falls back to CLI-local activeProfileId when GUI doesn’t supply env vars
  1. localEnvironmentVariables cache stored in settings (can contain secrets)
  1. Profile schema strictness (UUID ids + URL-only endpoints)
  1. expandEnvVars logs expanded values/defaults and uses non-bash empty semantics
  1. Bun runtime selection is incoherent in subprocess spawning
  1. SpawnSessionOptions.environmentVariables type is too narrow vs reality
  1. Deterministic message drop on disconnect for Claude session messages (early return)
  1. Offline stub is a plain object cast to ApiSessionClient

Summary by CodeRabbit

  • New Features

    • Richer environment-variable expansion (fallback and assign forms)
    • Environment-preview UI with configurable secret-masking policies
    • Sessions can include an optional profile identifier
  • Bug Fixes

    • Fail-fast validation for missing commands/inputs and improved exit-code/signal handling
    • More accurate CLI/source detection on Windows and safer subprocess output handling
  • Improvements

    • Profile schema migration consolidating environment variables
    • Reduced exposure of secrets in diagnostics and spawn logs
  • Tests

    • Expanded and more robust tests for tmux, spawn, and session behaviors

✏️ Tip: You can customize this high-level summary in your review settings.

Validation:

- Verified TMUX/TMUX_PANE values inside an isolated tmux server (custom socket)

- Verified kill-window syntax and new-session -t semantics via tmux manpage

Changes:

- Correct TMUX env parsing (socket_path + server_pid) and prefer TMUX_PANE for pane id

- Avoid invalid -t injection for commands that already specify a target; do not treat new-session as targetable

- Fix kill-window invocation to use -t session:window

- Align Machine.metadata typing with runtime (nullable)

- Make socket send behavior consistent when disconnected; tighten misc scripts/tests
Remove the hard send guard that dropped messages while disconnected and rely on socket.io client buffering (tests updated).
Make debugLargeJson a true DEBUG-only path and avoid logging expanded env values/defaults; tighten doctor masking for default templates.
Align happy-cli profile schema with the app: profiles are env-var based only. Migrate any legacy provider config fields into environmentVariables during parsing to avoid data loss.
Add a single runtime invocation builder for node vs bun and allow overriding via HAPPY_CLI_SUBPROCESS_RUNTIME.
The GUI sends a profile-derived env var map that may include provider-specific keys and tmux knobs; type it as Record<string,string>.
@coderabbitai
Copy link

coderabbitai bot commented Jan 17, 2026

📝 Walkthrough

Walkthrough

Consolidates env init, adds profileId plumbing and environment-preview RPC, migrates legacy profile configs to environmentVariables, refactors tmux utilities and daemon spawn paths, expands env-var expansion/masking, improves session/socket handling, and adds many tests across CLI, daemon, API, codex, tmux, persistence, and utils.

Changes

Cohort / File(s) Summary
CLI/script init & small utilities
bin/happy-dev.mjs, scripts/env-wrapper.cjs, scripts/ripgrep_launcher.cjs, scripts/test-continue-fix.sh
Consolidated unconditional HAPPY_* env initialization; added command validation/fail-fast in env-wrapper; ripgrep launcher now pipes stdout and has improved exit/signal handling and JSON-arg validation; test-continue-fix adds project-dir existence check.
Ripgrep tests
scripts/__tests__/ripgrep_launcher.test.ts
Introduced readLauncherFile() helper, replaced repeated reads, and added a test verifying signal-termination handling.
Windows source detection
scripts/claude_version_utils.cjs, scripts/claude_version_utils.test.ts
Tightened Windows npm detection to require scoped @anthropic-ai/claude-code segments and exclude hashed prerelease folders; updated test expectation to PATH for unrelated scoped packages.
API: spawn/session/profile plumbing
src/api/apiMachine.ts, src/api/types.ts, src/modules/common/registerCommonHandlers.ts
Propagates profileId into spawnSession; Machine.metadata now nullable and Metadata gains optional profileId; registerCommonHandlers adds preview-env and detect-cli handlers, exposes SpawnSessionOptions with profileId and environmentVariables (Record).
API session sending/logging & tests
src/api/apiSession.ts, src/api/apiSession.test.ts, src/api/api.test.ts
Unified logSendWhileDisconnected to dedupe disconnected logs and reset on connect; tests added/updated to cover buffering and moved console mock cleanup into try/finally.
Session metadata
src/utils/createSessionMetadata.ts
Reads HAPPY_SESSION_PROFILE_ID and includes nullable profileId in generated Metadata.
Offline session stub
src/utils/offlineSessionStub.ts, src/utils/offlineSessionStub.test.ts
Replaced object stub with OfflineSessionStub class implementing a stub contract and using RpcHandlerManager; added tests validating EventEmitter-like behavior.
Env var expansion
src/utils/expandEnvVars.ts, src/utils/expandEnvVars.test.ts
Added support for ${VAR:-default} and ${VAR:=default} semantics, assignedEnv caching for :=, warnOnUndefined option, and tests for default/assignment and empty-string handling.
Persistence / profile migration
src/persistence.ts, src/persistence.profileSchema.test.ts
Added normalizeLegacyProfileConfig and mergeEnvironmentVariables; migrated legacy provider configs into environmentVariables; bumped SUPPORTED_SCHEMA_VERSION to 3; removed CLI-local env cache and related getters/setters; added tests for migration behavior.
Claude & signal coordination
src/claude/claudeLocal.ts, src/claude/runClaude.ts, src/claude/utils/sessionScanner.test.ts
Added -r resume flag support (note: removed inner redeclaration risk in offline branch), wired AbortController for SIGINT/SIGTERM around claudeLocal, include profileId from env in Metadata, and made sessionScanner tests timing-robust via waitFor/getProjectPath.
Daemon tmux spawn & helpers
src/daemon/run.ts, src/daemon/run.tmuxEnv.test.ts, src/daemon/run.tmuxSpawn.test.ts
Added buildTmuxWindowEnv and buildTmuxSpawnConfig exports; integrated tmux spawn pathway using explicit tmuxEnv/tmuxCommandEnv; introduced Codex_HOME cleanup callbacks tracked by PID; sanitized spawn logging and enforced GUI extraEnv precedence; tests added.
Tmux utilities & tests (large refactor)
src/utils/tmux.ts, src/utils/tmux.test.ts, src/utils/tmux.socketPath.test.ts, src/utils/tmux.commandEnv.test.ts, src/utils/tmux.real.integration.test.ts
Major refactor: added normalizeExitCode, shell-quoting helpers, broadened name parsing to allow spaces, changed TmuxEnvironment shape (socket_path/server_pid/pane), added per-configuration TmuxUtilities via getTmuxUtilities, improved spawn/new-window behavior (literal KEY=VALUE env passing) and expanded tests including opt-in real integration suite.
Happy CLI subprocess invocation
src/utils/spawnHappyCLI.ts, src/utils/spawnHappyCLI.invocation.test.ts
Added buildHappyCliSubprocessInvocation to choose node vs bun runtime and build argv; spawnHappyCLI delegates to it; tests cover runtime-specific behavior.
Codex MCP client & elicitation
src/codex/codexMcpClient.ts, src/codex/codexMcpClient.test.ts
Added createCodexElicitationRequestHandler factory with zod validation and provider-based permission handling; broadened CLI version regex and fallback to 'mcp-server'; tests for elicitation and command detection.
UI doctor redaction & tests
src/ui/doctor.ts, src/ui/doctor.test.ts
Added maskValue and redactSettingsForDisplay to mask secrets (including template defaults); integrated into doctor output; tests for masking behavior.
Logger changes & tests
src/ui/logger.ts, src/ui/logger.test.ts
debugLargeJson now returns early when DEBUG unset (no production log); tests verify file writes only when DEBUG set.
Misc tests/docs cleanup
docs/bug-fix-plan-2025-01-15-athundt.md
Removed duplicate mock entry.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰 I hop through envs and tmux panes bright,

profile IDs tucked in by morning light,
Secrets masked in doctor’s gentle gaze,
Legacy configs find new env arrays,
Tests bloom like carrots—what a sight! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.91% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: stabilize PR107 tmux spawn, offline buffering, and bun runtime' accurately summarizes the main objectives of the changeset. It references the key areas addressed: tmux spawn fixes, offline buffering restoration, and bun runtime consistency.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

@gemini-code-assist
Copy link

Summary of Changes

Hello @leeroybrun, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request focuses on enhancing the stability, correctness, and security of the CLI's core functionalities, particularly around session management, environment variable handling, and integration with external tools like tmux and Bun. The changes aim to resolve several regressions and inconsistencies introduced in a previous update, ensuring a more robust and predictable user experience without exposing sensitive data.

Highlights

  • Tmux Stability and Correctness: Addressed multiple issues with tmux integration, including incorrect environment variable injection, non-deterministic session selection, improper parsing of TMUX environment variables, and command string construction that broke quoting. TMUX_TMPDIR is now properly honored for client invocations.
  • Offline Buffering Restoration: Restored reliable offline buffering for Socket.IO sends by removing early-return drops that caused complete message loss when disconnected, allowing messages to be buffered and sent upon reconnection.
  • Bun Runtime Coherence: Ensured consistent and coherent Bun subprocess spawning across both normal and tmux-based sessions, resolving issues where Bun was auto-selected but still used Node-only invocation patterns.
  • RPC Typing Alignment: Updated the spawn RPC typing to correctly accept arbitrary environment variable maps, aligning the contract with what the GUI actually sends.
  • Logging and Secret Redaction: Hardened logging by tightening DEBUG gating and redacting sensitive information (e.g., API tokens, environment variable values) from logs and the happy doctor output. Offline stubs were also made safer.
  • Profile Management Refinements: Removed unintended profile application from CLI settings, ensuring profile environment variables are only applied when explicitly provided. Introduced persistence for profile identity per session for cross-device visibility and aligned CLI profile schema to support URL-or-template strings and non-UUID IDs.
  • Cleanup of Unused Functionality: Removed startupBashScript and localEnvironmentVariables from the CLI profile schema and settings, along with unused helper APIs, to streamline the codebase and eliminate misleading functionality.
  • Tmux Environment Persistence: Implemented optional session-environment persistence for profile-defined environment variables within tmux sessions, while intentionally avoiding persistence of daemon-injected auth/session markers.
  • New Tmux Integration Tests: Added opt-in integration tests that exercise the actual tmux-calling code against real tmux processes to validate fixes and ensure correct behavior.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This is a substantial and high-quality pull request that addresses several critical issues related to tmux integration, offline buffering, runtime support for Bun, and security. The fixes are well-implemented, and the extensive refactoring, especially in tmux.ts and persistence.ts, greatly improves the codebase's correctness and maintainability. The addition of comprehensive tests, including opt-in integration tests for tmux, is commendable. The improved logging and secret redaction are also significant enhancements that improve the security and debuggability of the application. Overall, this is an excellent set of changes that significantly stabilizes and hardens the CLI.

Copy link

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

Caution

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

⚠️ Outside diff range comments (2)
src/claude/utils/sessionScanner.test.ts (1)

24-53: Use vi.stubEnv for safe environment variable isolation in tests.
Manual process.env mutation and restoration can leak state across test workers or if exceptions occur. Replace with Vitest's vi.stubEnv() and vi.unstubAllEnvs() for proper test isolation.

Import vi from vitest, remove the originalClaudeConfigDir variable, replace the manual save/restore logic with:

  • vi.stubEnv('CLAUDE_CONFIG_DIR', claudeConfigDir) in beforeEach
  • vi.unstubAllEnvs() in afterEach
Suggested changes
-import { describe, it, expect, beforeEach, afterEach } from 'vitest'
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
-  let originalClaudeConfigDir: string | undefined
   let claudeConfigDir: string
-    originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR;
     claudeConfigDir = join(testDir, 'claude-config');
-    process.env.CLAUDE_CONFIG_DIR = claudeConfigDir;
+    vi.stubEnv('CLAUDE_CONFIG_DIR', claudeConfigDir);
-    if (originalClaudeConfigDir === undefined) {
-      delete process.env.CLAUDE_CONFIG_DIR;
-    } else {
-      process.env.CLAUDE_CONFIG_DIR = originalClaudeConfigDir;
-    }
+    vi.unstubAllEnvs();
src/daemon/run.ts (1)

315-324: Protect auth token on disk with restrictive permissions.

auth.json contains sensitive credentials. Writing without explicit mode can leave it world‑readable under permissive umasks.

🔒 Suggested fix
-            await fs.writeFile(join(codexHomeDir.name, 'auth.json'), options.token);
+            await fs.writeFile(join(codexHomeDir.name, 'auth.json'), options.token, {
+              mode: 0o600,
+              encoding: 'utf8',
+            });
🤖 Fix all issues with AI agents
In `@src/codex/codexMcpClient.test.ts`:
- Around line 5-43: Remove the test-suite mocks (the vi.mock calls for
'child_process' execSync, '@modelcontextprotocol/sdk/types.js'
ElicitRequestSchema, '@modelcontextprotocol/sdk/client/stdio.js'
StdioClientTransport and its __transportInstances, and the
'@modelcontextprotocol/sdk/client/index.js' Client) and convert the file into an
integration-style test that only runs when the real Codex CLI/transport is
available: implement a runtime check (e.g., attempt to run the Codex CLI version
or probe the actual transport binary) and skip the suite when the CLI is not
found, then exercise the real StdioClientTransport and Client paths so
assertions hit real execution (rather than mocked) code paths.

In `@src/daemon/run.tmuxEnv.test.ts`:
- Around line 1-9: Add a file-level JSDoc header comment and replace the dynamic
import inside the test with a static top-level import; specifically, move the
line that does const runModule = (await import('@/daemon/run'))... out of the
test and instead add import * as runModule from '@/daemon/run' (or the
appropriate named import) at the top, then reference
runModule.buildTmuxWindowEnv in the test to call the buildTmuxWindowEnv function
and keep the test otherwise unchanged.

In `@src/daemon/run.tmuxSpawn.test.ts`:
- Around line 1-13: Add the required JSDoc file header at the very top of this
test file and replace the dynamic import inside the test with a static import
placed before the describe block (e.g. import { buildTmuxSpawnConfig } from
'@/daemon/run' or import * as runModule from '@/daemon/run'); then update the
test to call buildTmuxSpawnConfig (or runModule.buildTmuxSpawnConfig) directly
instead of using await import, and remove the in-test dynamic import and any
related type assertions. Ensure the new static import is the top-most
non-comment content and the JSDoc header appears above it.

In `@src/utils/tmux.commandEnv.test.ts`:
- Around line 1-4: Add a brief JSDoc header comment at the top of the test file
and move the TmuxUtilities import to the module-level imports (replace the
dynamic import usage of TmuxUtilities in the test body). Remove the
vi.resetModules() call and the dynamic import that loads TmuxUtilities (the code
that calls import('.../tmux.utilities') or similar) and update any references to
use the statically imported TmuxUtilities symbol; ensure any test setup that
relied on module reset is adapted to work with the static import (e.g., reset
spies/mocks in beforeEach).
- Around line 11-41: Remove the artificial child_process.spawn mock (the
spawnMock/getLastSpawnCall hoisted block and the vi.mock('child_process', ...)
call) so tests exercise the real subprocess behavior; update the tmux.commandEnv
tests to call the real spawn (or move the test into the real tmux integration
suite) and, if needed, start a temporary tmux server within the test and assert
environment variable propagation (use the existing spawn-based helpers rather
than spawnMock and reference spawnMock/getLastSpawnCall only to remove them).

In `@src/utils/tmux.socketPath.test.ts`:
- Around line 1-4: Add a JSDoc header at the top of
src/utils/tmux.socketPath.test.ts describing the test file's purpose and any
relevant test-scope notes; then address the dynamic import of TmuxUtilities:
either move the import for TmuxUtilities to a top-level static import if test
isolation via vi.resetModules() (used at vi.resetModules()) is not needed, or
retain the dynamic import (const { TmuxUtilities } = await
import('@/utils/tmux')) but add a brief inline comment explaining that
vi.resetModules() precedes it and is required for test isolation so the import
must remain dynamic—ensure the code keeps vi.resetModules() immediately before
the dynamic import if you choose the latter.
- Around line 11-41: This test currently mocks child_process.spawn via the
hoisted spawnMock/getLastSpawnCall and vi.mock('child_process') which violates
unit-test policy; either remove the spawnMock/getLastSpawnCall block and the
vi.mock('child_process') call so the test uses the real child_process.spawn, or
move the test into the integration test suite that exercises TmuxUtilities (and
when moving, wrap it with the existing "skip if tmux unavailable" guard) so
socket-path behavior is verified end-to-end rather than by mocking.
🧹 Nitpick comments (13)
src/ui/logger.test.ts (1)

35-44: Consider setting DEBUG before the import for clarity.

The test works correctly because debugLargeJson checks process.env.DEBUG at call-time. However, setting DEBUG after the import (line 37) but before the call (line 39) is slightly counterintuitive and could confuse future readers.

♻️ Suggested reorder for clarity
     it('writes to log file when DEBUG is set', async () => {
+        process.env.DEBUG = '1';
         const { logger } = (await import('@/ui/logger')) as typeof import('@/ui/logger');
-        process.env.DEBUG = '1';

         logger.debugLargeJson('[TEST] debugLargeJson', { secret: 'value' });
src/utils/expandEnvVars.test.ts (1)

161-171: Consider adding a cross-entry assignment propagation test.

This test validates := assignment reuse within the same value. Based on the implementation in expandEnvVars.ts, assignedEnv persists across entries within a single expandEnvironmentVariables call. A test verifying cross-entry propagation would strengthen coverage:

it('propagates ${VAR:=default} assignments to subsequent entries', () => {
    const envVars = {
        FIRST: '${MISSING_VAR:=assigned-value}',
        SECOND: '${MISSING_VAR}',
    };
    const result = expandEnvironmentVariables(envVars, {});
    expect(result).toEqual({
        FIRST: 'assigned-value',
        SECOND: 'assigned-value',
    });
});
src/modules/common/registerCommonHandlers.previewEnv.test.ts (1)

13-39: Add an explicit return type for createTestRpcManager.
Aligns with the repo’s strict TS typing expectations.

♻️ Suggested typing update
-function createTestRpcManager(params?: { scopePrefix?: string }) {
+function createTestRpcManager(
+    params?: { scopePrefix?: string }
+): {
+    call: <TResponse, TRequest>(method: string, request: TRequest) => Promise<TResponse>;
+} {
As per coding guidelines, ...
src/persistence.ts (1)

197-198: Consider adding a JSDoc comment explaining schema version semantics.

The inline note clarifies scope, but a brief JSDoc would help downstream maintainers understand when to bump this version.

📝 Suggested documentation improvement
 // Settings schema version: Integer for overall Settings structure compatibility
 // Incremented when Settings structure changes (e.g., adding profiles array was v1→v2)
 // Used for migration logic in readSettings()
-// NOTE: This is the schema for happy-cli's local settings file (not the Happy app's server-synced account settings).
+/**
+ * Schema version for happy-cli's local settings file.
+ * Increment when the Settings structure changes (not for profile schema changes).
+ * v1 → v2: Added profiles array
+ * v2 → v3: Removed localEnvironmentVariables cache
+ */
 export const SUPPORTED_SCHEMA_VERSION = 3;
src/claude/utils/sessionScanner.test.ts (1)

6-8: Prefer @/ alias for src imports.
Consider aligning the new getProjectPath import with the @/ alias convention for src paths. As per coding guidelines, ...

♻️ Suggested change
-import { getProjectPath } from './path'
+import { getProjectPath } from '@/claude/utils/path'
src/codex/codexMcpClient.ts (1)

23-29: Broaden version parsing regex to handle known output formats.

The current regex pattern doesn't match all known codex --version output formats. Based on actual CLI releases, the command outputs:

  • "codex-cli 0.77.0" ✓ (matches current pattern)
  • "@openai/codex, 0.79.0" ✗ (doesn't match—pattern expects "codex-cli|codex" followed by space)

Expanding the regex to handle the package scope and comma separator would prevent silent failures and incorrect fallback to mcp-server for unparseable version strings. Consider allowing optional separators (space, comma) and package scopes:

♻️ Suggested improvement
-        const match = version.match(/\b(?:codex-cli|codex)\s+v?(\d+\.\d+\.\d+(?:-alpha\.\d+)?)\b/i);
+        const match = version.match(/(?:`@openai`\/)?codex(?:-cli)?\s*[,\s]\s*v?(\d+\.\d+\.\d+(?:-alpha\.\d+)?)/i);
scripts/env-wrapper.cjs (1)

54-61: LGTM! Proper input validation added.

Good addition to validate the command argument before proceeding. This prevents confusing errors when the user forgets to provide a command.


Optional: Consider extracting usage output to reduce duplication.

The usage/example output is duplicated between the variant validation (lines 42-50) and the command validation (lines 55-59).

♻️ Optional DRY refactor
+function printUsage() {
+  console.error('Usage: node scripts/env-wrapper.js <stable|dev> <command> [...args]');
+  console.error('');
+  console.error('Variants:');
+  console.error('  stable - Production-ready version (data: ~/.happy/)');
+  console.error('  dev    - Development version (data: ~/.happy-dev/)');
+  console.error('');
+  console.error('Examples:');
+  console.error('  node scripts/env-wrapper.js stable daemon start');
+  console.error('  node scripts/env-wrapper.js dev auth login');
+}
+
 if (!variant || !VARIANTS[variant]) {
-  console.error('Usage: node scripts/env-wrapper.js <stable|dev> <command> [...args]');
-  console.error('');
-  console.error('Variants:');
-  console.error('  stable - Production-ready version (data: ~/.happy/)');
-  console.error('  dev    - Development version (data: ~/.happy-dev/)');
-  console.error('');
-  console.error('Examples:');
-  console.error('  node scripts/env-wrapper.js stable daemon start');
-  console.error('  node scripts/env-wrapper.js dev auth login');
+  printUsage();
   process.exit(1);
 }

 if (!command) {
-  console.error('Usage: node scripts/env-wrapper.js <stable|dev> <command> [...args]');
-  console.error('');
-  console.error('Examples:');
-  console.error('  node scripts/env-wrapper.js stable daemon start');
-  console.error('  node scripts/env-wrapper.js dev auth login');
+  printUsage();
   process.exit(1);
 }
src/utils/spawnHappyCLI.ts (1)

65-87: LGTM! Well-structured invocation builder.

Good extraction of the subprocess invocation logic into a reusable, testable function. The design:

  • Correctly applies --no-warnings --no-deprecation only for Node.js (these are Node-specific flags)
  • Validates entrypoint existence before returning
  • Returns a typed tuple enabling callers to inspect the runtime choice

Optional: Consider reordering existence check before building argv.

Minor suggestion for logical flow clarity:

♻️ Optional reorder
 export function buildHappyCliSubprocessInvocation(args: string[]): { runtime: 'node' | 'bun'; argv: string[] } {
   const projectRoot = projectPath();
   const entrypoint = join(projectRoot, 'dist', 'index.mjs');

+  // Sanity check of the entrypoint path exists
+  if (!existsSync(entrypoint)) {
+    const errorMessage = `Entrypoint ${entrypoint} does not exist`;
+    logger.debug(`[SPAWN HAPPY CLI] ${errorMessage}`);
+    throw new Error(errorMessage);
+  }
+
   // Use the same Node.js flags that the wrapper script uses
   const nodeArgs = [
     '--no-warnings',
     '--no-deprecation',
     entrypoint,
     ...args
   ];

-  // Sanity check of the entrypoint path exists
-  if (!existsSync(entrypoint)) {
-    const errorMessage = `Entrypoint ${entrypoint} does not exist`;
-    logger.debug(`[SPAWN HAPPY CLI] ${errorMessage}`);
-    throw new Error(errorMessage);
-  }
-
   const runtime = getSubprocessRuntime();
   const argv = runtime === 'node' ? nodeArgs : [entrypoint, ...args];
   return { runtime, argv };
 }
src/utils/tmux.real.integration.test.ts (1)

136-146: Prefer logger over console for debug cleanup output.

Console debug output in src/** tests conflicts with the logging guidance. Consider switching to the shared logger for these diagnostic messages.

♻️ Suggested change
-import { TmuxUtilities } from '@/utils/tmux';
+import { TmuxUtilities } from '@/utils/tmux';
+import { logger } from '@/ui/logger';

@@
-        console.error('[tmux-it] Failed to kill isolated tmux server', {
+        logger.debug('[tmux-it] Failed to kill isolated tmux server', {
             socketPath,
             status: result.status,
             stderr: result.stderr,
             error: result.error?.message,
         });

@@
-                console.error('[tmux-it] Failed to kill isolated tmux server via TMUX_TMPDIR', {
+                logger.debug('[tmux-it] Failed to kill isolated tmux server via TMUX_TMPDIR', {
                     tmuxTmpDir,
                     status: result.status,
                     stderr: result.stderr,
                     error: result.error?.message,
                 });
As per coding guidelines, src/** logging should go through the logger.

Also applies to: 309-316

src/utils/tmux.test.ts (3)

9-21: Use @/ alias for src imports.

Please align the tmux import with the repo alias convention for src paths.

♻️ Suggested change
-import {
+import {
     normalizeExitCode,
     parseTmuxSessionIdentifier,
     formatTmuxSessionIdentifier,
     validateTmuxSessionIdentifier,
     buildTmuxSessionIdentifier,
     createTmuxSession,
     TmuxSessionIdentifierError,
     extractSessionAndWindow,
     TmuxUtilities,
     type TmuxSessionIdentifier,
     type TmuxCommandResult,
-} from './tmux';
+} from '@/utils/tmux';
As per coding guidelines, src/** imports should use the `@/` alias.

309-316: Add an explicit return type to withTmuxEnv.

This helper should declare its return type explicitly for strict typing.

♻️ Suggested change
-    const withTmuxEnv = (value: string | undefined, fn: () => void, pane?: string | undefined) => {
+    const withTmuxEnv = (value: string | undefined, fn: () => void, pane?: string | undefined): void => {
As per coding guidelines, TypeScript helpers should use explicit return types.

485-620: Avoid mocking/spies in tests per repo policy.

The new vi.spyOn and FakeTmuxUtilities usage introduces mocking. Please refactor to use real tmux integration coverage (or reuse the opt‑in real‑tmux suite) instead of mocks.

As per coding guidelines, tests should avoid mocking and make real API calls.

src/daemon/run.ts (1)

36-82: Add JSDoc for new exported helpers.

These exported helpers should include JSDoc to document purpose, inputs, and outputs.

♻️ Suggested change
-export function buildTmuxWindowEnv(
+/**
+ * Build the environment to pass into a tmux window, merging daemon env and extra overrides.
+ */
+export function buildTmuxWindowEnv(
   daemonEnv: NodeJS.ProcessEnv,
   extraEnv: Record<string, string>,
 ): Record<string, string> {
@@
-export function buildTmuxSpawnConfig(params: {
+/**
+ * Build the tmux spawn command tokens and environment for the given agent + directory.
+ */
+export function buildTmuxSpawnConfig(params: {
As per coding guidelines, new exports should carry JSDoc.

Comment on lines +5 to +43
// NOTE: This test suite uses mocks because the real Codex CLI / MCP transport
// is not guaranteed to be available in CI or local test environments.
vi.mock('child_process', () => ({
execSync: vi.fn(),
}));

vi.mock('@modelcontextprotocol/sdk/types.js', () => ({
ElicitRequestSchema: {},
}));

vi.mock('@modelcontextprotocol/sdk/client/stdio.js', () => {
const instances: any[] = [];

class StdioClientTransport {
public command: string;
public args: string[];
public env: Record<string, string>;

constructor(opts: { command: string; args: string[]; env: Record<string, string> }) {
this.command = opts.command;
this.args = opts.args;
this.env = opts.env;
instances.push(this);
}
}

return { StdioClientTransport, __transportInstances: instances };
});

vi.mock('@modelcontextprotocol/sdk/client/index.js', () => {
class Client {
setNotificationHandler() { }
setRequestHandler() { }
async connect() { }
async close() { }
}

return { Client };
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid mocks in Vitest tests per project rules.

The test guidelines require real API calls and no mocking; these vi.mock blocks violate that and reduce confidence in CLI/transport behavior. Consider converting these to integration tests that run only when Codex CLI is available (skip otherwise) so the assertions still exercise real execution paths. Based on learnings.

🤖 Prompt for AI Agents
In `@src/codex/codexMcpClient.test.ts` around lines 5 - 43, Remove the test-suite
mocks (the vi.mock calls for 'child_process' execSync,
'@modelcontextprotocol/sdk/types.js' ElicitRequestSchema,
'@modelcontextprotocol/sdk/client/stdio.js' StdioClientTransport and its
__transportInstances, and the '@modelcontextprotocol/sdk/client/index.js'
Client) and convert the file into an integration-style test that only runs when
the real Codex CLI/transport is available: implement a runtime check (e.g.,
attempt to run the Codex CLI version or probe the actual transport binary) and
skip the suite when the CLI is not found, then exercise the real
StdioClientTransport and Client paths so assertions hit real execution (rather
than mocked) code paths.

Comment on lines +1 to +9
import { describe, it, expect } from 'vitest';

describe('daemon tmux env building', () => {
it('merges daemon process env and profile env for tmux windows', async () => {
const runModule = (await import('@/daemon/run')) as typeof import('@/daemon/run');
const merged = runModule.buildTmuxWindowEnv(
{ PATH: '/bin', HOME: '/home/user', UNDEFINED: undefined },
{ HOME: '/override', CUSTOM: 'x' }
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check if the test file exists
ls -la src/daemon/run.tmuxEnv.test.ts 2>&1 || echo "File not found"

Repository: leeroybrun/happy-cli

Length of output: 142


🏁 Script executed:

# Check if buildTmuxWindowEnv is exported from src/daemon/run.ts
rg "export.*buildTmuxWindowEnv|function buildTmuxWindowEnv" src/daemon/run.ts

Repository: leeroybrun/happy-cli

Length of output: 99


🏁 Script executed:

# Get the full test file to verify current state
cat -n src/daemon/run.tmuxEnv.test.ts

Repository: leeroybrun/happy-cli

Length of output: 826


Add file header JSDoc and use static imports at the top.

This test file is missing a header comment and violates the import guidelines by using a dynamic import inside the test function. Move the import to the top of the file as a static import.

♻️ Suggested update
+/**
+ * Tests tmux window environment merging for daemon sessions.
+ */
+import { buildTmuxWindowEnv } from '@/daemon/run';
 import { describe, it, expect } from 'vitest';
@@
-        const runModule = (await import('@/daemon/run')) as typeof import('@/daemon/run');
-        const merged = runModule.buildTmuxWindowEnv(
+        const merged = buildTmuxWindowEnv(
             { PATH: '/bin', HOME: '/home/user', UNDEFINED: undefined },
             { HOME: '/override', CUSTOM: 'x' }
         );
📝 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
import { describe, it, expect } from 'vitest';
describe('daemon tmux env building', () => {
it('merges daemon process env and profile env for tmux windows', async () => {
const runModule = (await import('@/daemon/run')) as typeof import('@/daemon/run');
const merged = runModule.buildTmuxWindowEnv(
{ PATH: '/bin', HOME: '/home/user', UNDEFINED: undefined },
{ HOME: '/override', CUSTOM: 'x' }
);
/**
* Tests tmux window environment merging for daemon sessions.
*/
import { buildTmuxWindowEnv } from '@/daemon/run';
import { describe, it, expect } from 'vitest';
describe('daemon tmux env building', () => {
it('merges daemon process env and profile env for tmux windows', async () => {
const merged = buildTmuxWindowEnv(
{ PATH: '/bin', HOME: '/home/user', UNDEFINED: undefined },
{ HOME: '/override', CUSTOM: 'x' }
);
🤖 Prompt for AI Agents
In `@src/daemon/run.tmuxEnv.test.ts` around lines 1 - 9, Add a file-level JSDoc
header comment and replace the dynamic import inside the test with a static
top-level import; specifically, move the line that does const runModule = (await
import('@/daemon/run'))... out of the test and instead add import * as runModule
from '@/daemon/run' (or the appropriate named import) at the top, then reference
runModule.buildTmuxWindowEnv in the test to call the buildTmuxWindowEnv function
and keep the test otherwise unchanged.

Comment on lines +1 to +13
import { describe, it, expect } from 'vitest';

describe('daemon tmux spawn config', () => {
const originalRuntimeOverride = process.env.HAPPY_CLI_SUBPROCESS_RUNTIME;
const originalPath = process.env.PATH;

it('uses merged env and bun runtime when configured', async () => {
process.env.HAPPY_CLI_SUBPROCESS_RUNTIME = 'bun';
process.env.PATH = '/bin';

try {
const runModule = (await import('@/daemon/run')) as typeof import('@/daemon/run');
const cfg = runModule.buildTmuxSpawnConfig({
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and read its content
if [ -f "src/daemon/run.tmuxSpawn.test.ts" ]; then
  echo "=== File exists, reading content ==="
  cat -n "src/daemon/run.tmuxSpawn.test.ts"
else
  echo "File not found at src/daemon/run.tmuxSpawn.test.ts"
  # Try to find similar test files
  fd -e "test.ts" "daemon" | head -20
fi

Repository: leeroybrun/happy-cli

Length of output: 1821


🏁 Script executed:

# Check the run.ts file to see if buildTmuxSpawnConfig is exported
if [ -f "src/daemon/run.ts" ]; then
  echo "=== Checking for buildTmuxSpawnConfig export ==="
  rg "export.*buildTmuxSpawnConfig" src/daemon/run.ts -A 2
fi

Repository: leeroybrun/happy-cli

Length of output: 220


🏁 Script executed:

# Verify the import path works
if [ -f "src/daemon/run.ts" ]; then
  echo "=== Checking module exports ==="
  head -50 src/daemon/run.ts | cat -n
fi

Repository: leeroybrun/happy-cli

Length of output: 2502


Add JSDoc file header and move import to top of file.

This test file is missing the required JSDoc header comment and violates the guideline that all imports must be at the top. Move the import statement before the describe block and remove the dynamic import from within the test.

♻️ Suggested update
+/**
+ * Tests daemon tmux spawn config construction.
+ */
+import { buildTmuxSpawnConfig } from '@/daemon/run';
 import { describe, it, expect } from 'vitest';
 
 describe('daemon tmux spawn config', () => {
@@ -11,8 +15,7 @@ describe('daemon tmux spawn config', () => {
         try {
-            const runModule = (await import('@/daemon/run')) as typeof import('@/daemon/run');
-            const cfg = runModule.buildTmuxSpawnConfig({
+            const cfg = buildTmuxSpawnConfig({
                 agent: 'claude',
                 directory: '/tmp',
📝 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
import { describe, it, expect } from 'vitest';
describe('daemon tmux spawn config', () => {
const originalRuntimeOverride = process.env.HAPPY_CLI_SUBPROCESS_RUNTIME;
const originalPath = process.env.PATH;
it('uses merged env and bun runtime when configured', async () => {
process.env.HAPPY_CLI_SUBPROCESS_RUNTIME = 'bun';
process.env.PATH = '/bin';
try {
const runModule = (await import('@/daemon/run')) as typeof import('@/daemon/run');
const cfg = runModule.buildTmuxSpawnConfig({
/**
* Tests daemon tmux spawn config construction.
*/
import { buildTmuxSpawnConfig } from '@/daemon/run';
import { describe, it, expect } from 'vitest';
describe('daemon tmux spawn config', () => {
const originalRuntimeOverride = process.env.HAPPY_CLI_SUBPROCESS_RUNTIME;
const originalPath = process.env.PATH;
it('uses merged env and bun runtime when configured', async () => {
process.env.HAPPY_CLI_SUBPROCESS_RUNTIME = 'bun';
process.env.PATH = '/bin';
try {
const cfg = buildTmuxSpawnConfig({
🤖 Prompt for AI Agents
In `@src/daemon/run.tmuxSpawn.test.ts` around lines 1 - 13, Add the required JSDoc
file header at the very top of this test file and replace the dynamic import
inside the test with a static import placed before the describe block (e.g.
import { buildTmuxSpawnConfig } from '@/daemon/run' or import * as runModule
from '@/daemon/run'); then update the test to call buildTmuxSpawnConfig (or
runModule.buildTmuxSpawnConfig) directly instead of using await import, and
remove the in-test dynamic import and any related type assertions. Ensure the
new static import is the top-most non-comment content and the JSDoc header
appears above it.

Comment on lines +1 to +4
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { EventEmitter } from 'node:events';
import type { SpawnOptions, ChildProcessWithoutNullStreams } from 'node:child_process';

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

head -70 src/utils/tmux.commandEnv.test.ts | cat -n

Repository: leeroybrun/happy-cli

Length of output: 2255


🏁 Script executed:

fd -e test.ts src/ | head -5 | xargs -I {} head -15 {}

Repository: leeroybrun/happy-cli

Length of output: 385


🏁 Script executed:

fd --full-path 'src/.*\.test\.ts$' | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && head -20 {}'

Repository: leeroybrun/happy-cli

Length of output: 3364


🏁 Script executed:

cd src && find . -name "*.test.ts" -exec grep -l "JSDoc\|\/\*\*" {} \; | wc -l && find . -name "*.test.ts" | wc -l

Repository: leeroybrun/happy-cli

Length of output: 69


🏁 Script executed:

cd src && find . -name "*.test.ts" -exec grep -l "/\*\*" {} \; | head -3 | xargs -I {} sh -c 'echo "=== {} ===" && head -10 {}'

Repository: leeroybrun/happy-cli

Length of output: 1720


🏁 Script executed:

cat CLAUDE.md 2>/dev/null || echo "File not found"

Repository: leeroybrun/happy-cli

Length of output: 8628


Add a file header JSDoc and move TmuxUtilities import to the top.

This file should include a brief header comment and import TmuxUtilities statically at the top. Remove the vi.resetModules() call and dynamic import at line 49-50, keeping all imports at the module level as required by the coding guidelines.

♻️ Suggested update
+/**
+ * Tests tmux subprocess environment handling in TmuxUtilities.
+ */
 import { describe, it, expect, vi, beforeEach } from 'vitest';
 import { EventEmitter } from 'node:events';
 import type { SpawnOptions, ChildProcessWithoutNullStreams } from 'node:child_process';
+import { TmuxUtilities } from '@/utils/tmux';
@@
-        vi.resetModules();
-        const { TmuxUtilities } = await import('@/utils/tmux');
-
         const utils = new TmuxUtilities('happy', { TMUX_TMPDIR: '/custom/tmux' });
         await utils.executeTmuxCommand(['list-sessions']);
🤖 Prompt for AI Agents
In `@src/utils/tmux.commandEnv.test.ts` around lines 1 - 4, Add a brief JSDoc
header comment at the top of the test file and move the TmuxUtilities import to
the module-level imports (replace the dynamic import usage of TmuxUtilities in
the test body). Remove the vi.resetModules() call and the dynamic import that
loads TmuxUtilities (the code that calls import('.../tmux.utilities') or
similar) and update any references to use the statically imported TmuxUtilities
symbol; ensure any test setup that relied on module reset is adapted to work
with the static import (e.g., reset spies/mocks in beforeEach).

Comment on lines +1 to +4
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { EventEmitter } from 'node:events';
import type { SpawnOptions, ChildProcessWithoutNullStreams } from 'node:child_process';

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/utils/tmux.socketPath.test.ts

Repository: leeroybrun/happy-cli

Length of output: 184


🏁 Script executed:

cat -n src/utils/tmux.socketPath.test.ts

Repository: leeroybrun/happy-cli

Length of output: 2320


Add a file header JSDoc comment.

This test file is missing the required header comment explaining its purpose. Additionally, the dynamic import at line 50 (const { TmuxUtilities } = await import('@/utils/tmux')) violates the guideline that ALL imports must be at the top of the file. However, note that it's preceded by vi.resetModules() at line 49, which is a legitimate test isolation pattern. If moving to a static top-level import, ensure the module reset behavior isn't needed for test correctness.

Suggested update
+/**
+ * Tests tmux socket-path usage in TmuxUtilities.
+ */
 import { describe, it, expect, vi, beforeEach } from 'vitest';
 import { EventEmitter } from 'node:events';
 import type { SpawnOptions, ChildProcessWithoutNullStreams } from 'node:child_process';
+import { TmuxUtilities } from '@/utils/tmux';
📝 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
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { EventEmitter } from 'node:events';
import type { SpawnOptions, ChildProcessWithoutNullStreams } from 'node:child_process';
/**
* Tests tmux socket-path usage in TmuxUtilities.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { EventEmitter } from 'node:events';
import type { SpawnOptions, ChildProcessWithoutNullStreams } from 'node:child_process';
import { TmuxUtilities } from '@/utils/tmux';
🤖 Prompt for AI Agents
In `@src/utils/tmux.socketPath.test.ts` around lines 1 - 4, Add a JSDoc header at
the top of src/utils/tmux.socketPath.test.ts describing the test file's purpose
and any relevant test-scope notes; then address the dynamic import of
TmuxUtilities: either move the import for TmuxUtilities to a top-level static
import if test isolation via vi.resetModules() (used at vi.resetModules()) is
not needed, or retain the dynamic import (const { TmuxUtilities } = await
import('@/utils/tmux')) but add a brief inline comment explaining that
vi.resetModules() precedes it and is required for test isolation so the import
must remain dynamic—ensure the code keeps vi.resetModules() immediately before
the dynamic import if you choose the latter.

Handle codex --version output variations without misreporting 'not installed' and remove stdout logging of elicitation payloads.
Provide an EventEmitter-compatible stub and a focused unit test so offline mode can't crash on basic session events.
- Treat TMUX_TMPDIR as a directory via tmux client env (not -S socket path)

- Pass per-window env via new-window -e KEY=value without shell-style quoting

- Make empty sessionName resolve deterministically (attached/most-recent)

- Preserve failure semantics for signal-terminated tmux commands
@leeroybrun leeroybrun force-pushed the slopus/pr/pr107-followup-fixes branch from df67085 to 0ff0b37 Compare January 17, 2026 15:22
Copy link

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

🤖 Fix all issues with AI agents
In `@src/modules/common/registerCommonHandlers.ts`:
- Around line 351-362: The code builds a RegExp from the environment value
HAPPY_ENV_PREVIEW_SECRET_NAME_REGEX (overrideRegexRaw) which risks ReDoS; update
secretNameRegex creation to validate and sanitize the user-provided pattern
before constructing a RegExp: reject or fallback to defaultSecretNameRegex if
the string is unusually long (e.g., >200 chars), contains potentially dangerous
constructs (e.g., nested quantifiers like (?:.*+){n} or catastrophic
backtracking patterns), or fails a safety check from a vetted library (e.g.,
safe-regex); keep the existing try/catch but add the pre-check on
overrideRegexRaw/trimmed and only construct new RegExp when the safety check
passes, otherwise log a warning and return defaultSecretNameRegex (affecting
variables: overrideRegexRaw, trimmed, secretNameRegex, defaultSecretNameRegex).

In `@src/utils/tmux.test.ts`:
- Around line 8-21: The tests for createTmuxSession and spawnInTmux currently
use vi.spyOn and a FakeTmuxUtilities subclass to stub tmux behavior; remove
those mocks and rewrite the tests to either invoke real tmux via the actual
TmuxUtilities implementation (so they exercise createTmuxSession and spawnInTmux
through real subprocess calls) or move these test cases into a separate
integration test suite and mark them accordingly; specifically eliminate usage
of vi.spyOn(...) and the FakeTmuxUtilities class, update references to
createTmuxSession and spawnInTmux to use the real TmuxUtilities, and ensure any
environment/setup/teardown needed for real tmux subprocesses is performed in the
test harness.
♻️ Duplicate comments (3)
src/codex/codexMcpClient.test.ts (1)

5-43: Replace mocks with integration-style Vitest tests per project rules.

Project guidance requires real API calls and no mocking; these vi.mock blocks violate that and can mask regressions. Convert to an integration-style suite that skips when Codex CLI/transport is unavailable. Based on learnings.

src/utils/tmux.commandEnv.test.ts (2)

1-4: Add a file header JSDoc and keep imports static.

The dynamic import plus vi.resetModules() conflicts with the “imports at top” rule, and the file still lacks a header comment. Prefer a module‑level TmuxUtilities import and reset spies/mocks in beforeEach if needed.

♻️ Proposed update
+/**
+ * Tests tmux subprocess environment handling.
+ */
 import { describe, it, expect, vi, beforeEach } from 'vitest';
 import { EventEmitter } from 'node:events';
 import type { SpawnOptions, ChildProcessWithoutNullStreams } from 'node:child_process';
+import { TmuxUtilities } from '@/utils/tmux';
@@
-        vi.resetModules();
-        const { TmuxUtilities } = await import('@/utils/tmux');
-
         const utils = new TmuxUtilities('happy', { TMUX_TMPDIR: '/custom/tmux' });

As per coding guidelines, please keep imports at the top and add a file header JSDoc.

Also applies to: 49-50


11-41: Avoid mocking child_process.spawn in tests.

This uses vi.mock/vi.fn to stub subprocess behavior, but the guidelines require real API calls with no mocking. The opt‑in tmux integration suite already covers TMUX_TMPDIR propagation, so consider moving this assertion there or exercising a real tmux server.

As per coding guidelines, unit tests should not mock subprocess APIs.

🧹 Nitpick comments (2)
src/codex/codexMcpClient.ts (2)

24-29: Validate fallback to mcp-server for unparseable Codex version output.

Defaulting to mcp-server when the version format is unexpected could break older Codex installs that only support mcp. Please verify this behavior against real CLI outputs, and consider a runtime fallback (e.g., try mcp-server, then mcp if spawn fails).


52-120: Preserve denial context from permission handler (if available).

If handleToolCall can return a reason, consider threading it through so callers can surface meaningful denial/abort context instead of dropping it.

Comment on lines +351 to +362
const defaultSecretNameRegex = /TOKEN|KEY|SECRET|AUTH|PASS|PASSWORD|COOKIE/i;
const overrideRegexRaw = process.env.HAPPY_ENV_PREVIEW_SECRET_NAME_REGEX;
const secretNameRegex = (() => {
if (typeof overrideRegexRaw !== 'string') return defaultSecretNameRegex;
const trimmed = overrideRegexRaw.trim();
if (!trimmed) return defaultSecretNameRegex;
try {
return new RegExp(trimmed, 'i');
} catch {
return defaultSecretNameRegex;
}
})();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential ReDoS vulnerability with user-controlled regex.

The static analysis correctly flags that constructing a RegExp from HAPPY_ENV_PREVIEW_SECRET_NAME_REGEX environment variable could enable Regular Expression Denial of Service if a malicious pattern is provided.

While this is an environment variable (requiring daemon access to set), consider adding basic safety measures:

🛡️ Suggested mitigation
             const secretNameRegex = (() => {
                 if (typeof overrideRegexRaw !== 'string') return defaultSecretNameRegex;
                 const trimmed = overrideRegexRaw.trim();
                 if (!trimmed) return defaultSecretNameRegex;
+                // Limit regex length to mitigate ReDoS risk
+                if (trimmed.length > 200) {
+                    logger.warn('[preview-env] HAPPY_ENV_PREVIEW_SECRET_NAME_REGEX too long, using default');
+                    return defaultSecretNameRegex;
+                }
                 try {
                     return new RegExp(trimmed, 'i');
                 } catch {
                     return defaultSecretNameRegex;
                 }
             })();

Alternatively, you could use a library like safe-regex to validate the pattern, though the risk is lower since this requires daemon-level access to exploit.

📝 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
const defaultSecretNameRegex = /TOKEN|KEY|SECRET|AUTH|PASS|PASSWORD|COOKIE/i;
const overrideRegexRaw = process.env.HAPPY_ENV_PREVIEW_SECRET_NAME_REGEX;
const secretNameRegex = (() => {
if (typeof overrideRegexRaw !== 'string') return defaultSecretNameRegex;
const trimmed = overrideRegexRaw.trim();
if (!trimmed) return defaultSecretNameRegex;
try {
return new RegExp(trimmed, 'i');
} catch {
return defaultSecretNameRegex;
}
})();
const defaultSecretNameRegex = /TOKEN|KEY|SECRET|AUTH|PASS|PASSWORD|COOKIE/i;
const overrideRegexRaw = process.env.HAPPY_ENV_PREVIEW_SECRET_NAME_REGEX;
const secretNameRegex = (() => {
if (typeof overrideRegexRaw !== 'string') return defaultSecretNameRegex;
const trimmed = overrideRegexRaw.trim();
if (!trimmed) return defaultSecretNameRegex;
// Limit regex length to mitigate ReDoS risk
if (trimmed.length > 200) {
logger.warn('[preview-env] HAPPY_ENV_PREVIEW_SECRET_NAME_REGEX too long, using default');
return defaultSecretNameRegex;
}
try {
return new RegExp(trimmed, 'i');
} catch {
return defaultSecretNameRegex;
}
})();
🧰 Tools
🪛 ast-grep (0.40.5)

[warning] 357-357: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(trimmed, 'i')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🤖 Prompt for AI Agents
In `@src/modules/common/registerCommonHandlers.ts` around lines 351 - 362, The
code builds a RegExp from the environment value
HAPPY_ENV_PREVIEW_SECRET_NAME_REGEX (overrideRegexRaw) which risks ReDoS; update
secretNameRegex creation to validate and sanitize the user-provided pattern
before constructing a RegExp: reject or fallback to defaultSecretNameRegex if
the string is unusually long (e.g., >200 chars), contains potentially dangerous
constructs (e.g., nested quantifiers like (?:.*+){n} or catastrophic
backtracking patterns), or fails a safety check from a vetted library (e.g.,
safe-regex); keep the existing try/catch but add the pre-check on
overrideRegexRaw/trimmed and only construct new RegExp when the safety check
passes, otherwise log a warning and return defaultSecretNameRegex (affecting
variables: overrideRegexRaw, trimmed, secretNameRegex, defaultSecretNameRegex).

Comment on lines +8 to 21
import { describe, expect, it, vi } from 'vitest';
import {
normalizeExitCode,
parseTmuxSessionIdentifier,
formatTmuxSessionIdentifier,
validateTmuxSessionIdentifier,
buildTmuxSessionIdentifier,
createTmuxSession,
TmuxSessionIdentifierError,
extractSessionAndWindow,
TmuxUtilities,
type TmuxSessionIdentifier,
type TmuxCommandResult,
} from './tmux';
Copy link

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:

wc -l src/utils/tmux.test.ts

Repository: leeroybrun/happy-cli

Length of output: 90


🏁 Script executed:

cat -n src/utils/tmux.test.ts

Repository: leeroybrun/happy-cli

Length of output: 27932


Remove mocking from createTmuxSession and spawnInTmux tests to comply with the no-mocking guideline.

Tests for createTmuxSession (lines 486–498) and spawnInTmux (lines 501–621) violate the requirement to write unit tests without mocking. The vi.spyOn at line 487 and the FakeTmuxUtilities subclass at line 502 stub tmux execution paths instead of making real API calls. Use actual tmux subprocess execution in tests or move these to an integration suite.

🤖 Prompt for AI Agents
In `@src/utils/tmux.test.ts` around lines 8 - 21, The tests for createTmuxSession
and spawnInTmux currently use vi.spyOn and a FakeTmuxUtilities subclass to stub
tmux behavior; remove those mocks and rewrite the tests to either invoke real
tmux via the actual TmuxUtilities implementation (so they exercise
createTmuxSession and spawnInTmux through real subprocess calls) or move these
test cases into a separate integration test suite and mark them accordingly;
specifically eliminate usage of vi.spyOn(...) and the FakeTmuxUtilities class,
update references to createTmuxSession and spawnInTmux to use the real
TmuxUtilities, and ensure any environment/setup/teardown needed for real tmux
subprocesses is performed in the test harness.

@ahundt
Copy link

ahundt commented Jan 26, 2026

I'm glad to see progress on outstanding bugs! Sorry about the log var leakage.

I'm a bit surprised by the ai code review claim that env variables were nondeterministic. I had intended to set it up so if it is not assigned it uses the remote value and shows it to you, and if you want to override just set the value. That seemed deterministic in concept to me but maybe there was a bug?

Or perhaps in the sense thay on a second machine with different variables set the outcome is different?

I've also got a few bugfix commits but I didn't want to introduce new merge conflicts as I saw others were actively changing that code so I haven't submitted it yet, like the bug in saves which I think this PR at least partly addresses.

Either way cool stuff and thanks for addressing these items!

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.

2 participants