Skip to content

feat: add optional sound notifications on run lifecycle events - #96

Merged
pedronauck merged 2 commits into
compozy:mainfrom
brunomguimaraes:feat/sound-notifications
Apr 15, 2026
Merged

pedronauck merged 2 commits into
compozy:mainfrom
brunomguimaraes:feat/sound-notifications

Conversation

@brunomguimaraes

@brunomguimaraes brunomguimaraes commented Apr 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds optional audio cues that play when an ACP run reaches a terminal state — a small ergonomic win for operators who step away from long-running sessions and want to know without checking the terminal whether the model finished cleanly or failed.

The feature is opt-in via a new [sound] section in .compozy/config.toml and ships disabled by default, so it costs nothing for users who don't want it.

[sound]
enabled = true
on_completed = "glass"   # preset name OR absolute path
on_failed = "basso"      # played on run.failed and run.cancelled
sound.mp4

Presets resolve to platform-native files at play time (afplay on darwin, paplay on linux, powershell on windows). Absolute paths bypass preset lookup so users can ship custom sounds.

Why

  • A long ACP session can run for minutes; switching back to the terminal only to check a status string is friction.
  • A 500ms beep at the boundary is enough to surface the result without requiring focus.
  • Keeping it strictly opt-in means CI, headless servers, and silence-loving users see no behavior change.

Design notes

Synchronous playback at the emission point. The first iteration used a goroutine subscribed to the run event bus, but cleanup raced the terminal event and could silently drop the very sound it was meant to play. The final design exposes sound.Notify(ctx, cfg, kind, logger) which plays on the caller's goroutine right after emitRunTerminalEvent (executor) and submitRuntimeEvent (exec). It blocks for ~500ms on the audio command, then returns. Single-flight, deterministic, no concurrency surface.

Cross-platform isolation via build tags. player_unix.go (//go:build !windows) selects afplay/paplay based on runtime.GOOS; player_windows.go shells out to powershell + System.Media.SoundPlayer. Other unix variants gracefully fall back to a Noop player.

Test seam. os/exec is hidden behind a commandRunner interface so unit tests never shell out — they assert the resolved command name and args without producing audio.

TOML-only for v1. No CLI flag was added; per-project sound feels like a project preference rather than a per-invocation toggle. Easy to add later if there's demand.

Files touched

Area Files
New package internal/core/sound/{player,runner,presets,player_unix,player_windows,subscriber}.go (+ tests)
Config schema internal/core/workspace/config_types.go, config_validate.go, config_test.go
Runtime config internal/core/model/runtime_config.go, model_test.go
Executor wiring internal/core/run/executor/execution.go (synchronous notifySoundForKind after emitRunTerminalEvent)
Exec wiring internal/core/run/exec/exec.go (notifySoundForKind inside submitRuntimeEvent)
Plumbing internal/core/run/internal/runshared/config.go, internal/core/api.go, internal/cli/state.go, internal/cli/workspace_config.go
SDK alignment sdk/extension/types.go (mirrors model.RuntimeConfig per the existing compat_test contract)

Test plan

  • make verify passes (1848 tests, 0 lint, build green)
  • New tests cover preset resolution per OS, Notify for every terminal kind, silent kinds, nil player, nil context, playback errors logged not panicked
  • Extended TestRuntimeConfigApplyDefaults to cover the sound default branch (presets only fill when SoundEnabled is true; explicit values are preserved)
  • New TestLoadConfig cases for [sound] parsing, default-disabled state, and empty-string rejection
  • Manual: compozy exec --dry-run --tui=false "hello" plays Glass on darwin
  • Manual: compozy exec --tui --dry-run "hello" plays Glass with the cockpit visible
  • Manual: feature is silent when enabled = false or the section is absent
  • Maintainer to verify on linux (paplay + freedesktop sounds) — only tested darwin

Not included

  • "Awaiting input" event kind. The current run lifecycle has no native "blocked on user feedback" event; adding one is a larger surface change and didn't seem worth bundling. Happy to follow up in a second PR if there's interest.
  • Job-level beeps. The wiring is intentionally restricted to the three run-level events to avoid a 50-beep batch run.
  • Volume / debounce controls. Easy to add if real-world usage shows a need.

Summary by CodeRabbit

  • New Features
    • Sound notifications for run completion/failure across Windows, macOS, and Linux.
    • New [sound] project config to enable/disable notifications and set completion/failure sounds.
    • Configurable defaults applied when enabled (defaults: glass for success, basso for failure); preset names are trimmed and preserved when explicitly set.
    • Preset list: glass, basso, ping, hero, funk, tink, submarine.
    • Runtime SDK exposes sound-enabled fields for extensions.

@coderabbitai

coderabbitai Bot commented Apr 14, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@brunomguimaraes has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 46 minutes and 48 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 46 minutes and 48 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e6c3712f-2836-4757-b843-ab90fc7cb1fc

📥 Commits

Reviewing files that changed from the base of the PR and between 7a4329e and 33a20a7.

📒 Files selected for processing (6)
  • internal/core/sound/player.go
  • internal/core/sound/player_test.go
  • internal/core/sound/presets.go
  • internal/core/sound/presets_test.go
  • internal/core/sound/subscriber_test.go
  • internal/core/workspace/config_test.go

Walkthrough

Adds an opt-in run-lifecycle sound notification system: new sound config fields throughout CLI, core, SDK, workspace config parsing/validation, a new internal/core/sound package (players, presets, runner, notifier), tests, and hooks that trigger sounds on run completion/failure during finalization.

Changes

Cohort / File(s) Summary
CLI Configuration
internal/cli/state.go, internal/cli/workspace_config.go
Added runtime state fields (soundEnabled, soundOnCompleted, soundOnFailed) and an applySoundConfig helper to populate command state from workspace sound config.
Public/Core Config & Runtime Model
internal/core/api.go, internal/core/model/runtime_config.go, internal/core/model/model_test.go
Extended exported core.Config and model.RuntimeConfig with sound fields; added default constants and ApplyDefaults trimming/conditional defaults; added unit tests for defaulting/trim behavior.
Workspace Config Types & Validation
internal/core/workspace/config_types.go, internal/core/workspace/config_validate.go, internal/core/workspace/config_test.go
Added SoundConfig to ProjectConfig, pointer fields for optional enabled, on_completed, on_failed, validation helpers to reject empty/whitespace values, and tests for parsing/validation.
Execution Pipeline Integration
internal/core/run/internal/runshared/config.go, internal/core/run/exec/exec.go, internal/core/run/executor/execution.go
Propagated sound settings into run config, stored runtime config on exec state, invoked notifySoundForKind in exec and executor finalization paths to trigger notifications.
Sound Playback Infrastructure
internal/core/sound/player.go, internal/core/sound/runner.go, internal/core/sound/player_unix.go, internal/core/sound/player_windows.go, internal/core/sound/player_test.go
New sound package: Player interface, Noop and OS-backed osPlayer, execRunner invoking platform commands, resolution of commands per-OS, and tests covering runner/player behavior.
Sound Presets & Resolution
internal/core/sound/presets.go, internal/core/sound/presets_test.go
Defined preset constants, OS-specific preset path maps, ResolvePath and KnownPresets() with tests verifying absolute-path passthrough, preset resolution, and error messages.
Notification Subscriber
internal/core/sound/subscriber.go, internal/core/sound/subscriber_test.go
Implemented Notify(ctx, cfg, kind, logger) and Config (Player + OnCompleted/OnFailed), playback timeout/detach semantics, pickSound mapping, and comprehensive tests including logging and context-detach behavior.
SDK Types
sdk/extension/types.go
Added SoundEnabled, SoundOnCompleted, SoundOnFailed to exported extension.RuntimeConfig.

Sequence Diagram

sequenceDiagram
    participant CLI as CLI State
    participant Exec as Execution Engine
    participant Final as FinalizeExecution
    participant Notif as Sound.Notifier
    participant Player as Sound.Player
    participant OS as OS Command

    CLI->>Exec: pass runtime config (SoundEnabled, OnCompleted, OnFailed)
    Exec->>Final: finalizeExecution(result)
    Final->>Final: emit terminal event
    Final->>Notif: notifySoundForKind(ctx, kind, cfg)
    Notif->>Notif: pickSound(kind, cfg)
    Notif->>Player: Player.Play(ctx, sound)
    Player->>Player: ResolvePath / map preset
    Player->>OS: run platform command (afplay/paplay/powershell)
    OS-->>Player: return success|error
    Player-->>Notif: return error|nil
    Notif->>Notif: log debug on error
    Final->>Exec: continue post-shutdown hooks
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • PR #80: Touches internal/cli/state.go to add commandState fields and buildConfig wiring; overlaps CLI runtime-config plumbing.
  • PR #40: Modifies workspace config application flow used by internal/cli/workspace_config.go; related to applying project-level defaults.
  • PR #70: Adjusts runtime-config conversion paths (internal/core/api.go, model/runtime_config.go) similar to the core runtime additions here.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.30% 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 'feat: add optional sound notifications on run lifecycle events' directly and clearly summarizes the main change: adding an opt-in audio notification feature for run completion, failure, and cancellation.

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

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

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

❤️ Share

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

@brunomguimaraes
brunomguimaraes force-pushed the feat/sound-notifications branch 2 times, most recently from e06edb0 to 6bb6543 Compare April 14, 2026 00:34
@pedronauck

Copy link
Copy Markdown
Member

this is a nice feature, there's a reason it's still in draft mode?

@brunomguimaraes

Copy link
Copy Markdown
Contributor Author

this is a nice feature, there's a reason it's still in draft mode?

Yes, I haven't had the opportuity to fully test in other OS

@brunomguimaraes
brunomguimaraes force-pushed the feat/sound-notifications branch from 6bb6543 to ded7f4a Compare April 14, 2026 17:56
@pedronauck

Copy link
Copy Markdown
Member

if you tested on macOS is fine for this first version @brunomguimaraes

@brunomguimaraes
brunomguimaraes marked this pull request as ready for review April 14, 2026 19:52

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (7)
internal/core/workspace/config_test.go (1)

649-710: Please fold the new sound cases into table-driven subtests.

These assertions are fine, but this file already uses table-driven t.Run(...) coverage for config variants. Grouping the sound scenarios the same way will make it easier to extend coverage for both presets and validation branches without adding more one-off top-level tests. As per coding guidelines, **/*_test.go: Table-driven tests with subtests (t.Run) as the default patternandMUST use t.Run("Should...") pattern for ALL test cases`.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/workspace/config_test.go` around lines 649 - 710, Replace the
three separate top-level tests (TestLoadConfigParsesSoundSection,
TestLoadConfigLeavesSoundDisabledByDefault,
TestLoadConfigRejectsEmptySoundPreset) with a single table-driven test (e.g.,
TestLoadConfig_SoundCases) that defines a slice of cases containing a
descriptive name, the workspace config string, expected error flag, and expected
Sound fields; iterate cases and call t.Run("Should ...", func(t *testing.T) {
... }) for each, create temp dir, writeWorkspaceConfig, call LoadConfig, and
assert expectations (cfg.Sound.Enabled, OnCompleted, OnFailed or error contents
like "sound.on_completed") inside each subtest so all scenarios are covered
uniformly and follow the required t.Run("Should...") pattern.
internal/core/sound/player.go (1)

61-64: Simplify redundant error check.

The error check wrapping runner.Run can be simplified since you're just returning the error directly.

♻️ Simplified return
-	if err := p.runner.Run(ctx, name, args...); err != nil {
-		return err
-	}
-	return nil
+	return p.runner.Run(ctx, name, args...)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/sound/player.go` around lines 61 - 64, The current pattern
checks the error from p.runner.Run then returns it, which is redundant; replace
the if-block with a direct return of the call — i.e., return p.runner.Run(ctx,
name, args...) — to simplify the code in the method that calls p.runner.Run.
internal/core/sound/presets.go (2)

18-18: Consider using full name for consistency: PresetSubmarine.

The constant name PresetSubmar appears truncated compared to other presets (PresetGlass, PresetBasso, etc.). While the value is correctly "submarine", the constant name inconsistency could cause confusion when developers reference it in code.

♻️ Suggested rename
-	PresetSubmar = "submarine"
+	PresetSubmarine = "submarine"

Then update references in KnownPresets() and all preset maps.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/sound/presets.go` at line 18, Rename the truncated constant
PresetSubmar to PresetSubmarine for consistency; update its declaration and
every reference (including in KnownPresets() and all preset maps/variables that
use PresetSubmar) to the new identifier PresetSubmarine, ensuring the string
value remains "submarine" and run/rebuild to catch any missed usages.

115-125: Consider resolving Windows presets using the WINDIR environment variable at runtime.

The hardcoded C:\Windows path will fail on non-standard Windows installations where WINDIR points to a different drive. While the comment documents this limitation and users can provide absolute paths as a workaround, using os.Getenv("WINDIR") would make the feature more robust for non-standard setups. This is a low-priority enhancement given the documented workaround exists.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/sound/presets.go` around lines 115 - 125, The windowsPresets
map currently uses hardcoded "C:\Windows\Media\..." paths which can break on
machines where WINDIR is different; update the code so windowsPresets is built
at runtime using os.Getenv("WINDIR") (fallback to "C:\\Windows" if empty) to
join with "Media" and the filenames (e.g., build the map in an init function or
a helper like newWindowsPresets) and keep the same keys (PresetGlass,
PresetBasso, PresetPing, PresetHero, PresetFunk, PresetTink, PresetSubmar) so
callers are unchanged. Ensure path construction uses filepath.Join on Windows.
internal/core/sound/player_test.go (2)

37-45: Consider using table-driven subtests for consistency.

This test covers two distinct cases (empty string and preset name) inline. While the test is simple, using t.Run subtests would align with coding guidelines and improve failure diagnostics.

♻️ Optional refactor to use t.Run
 func TestNoop_Play_AlwaysNil(t *testing.T) {
 	t.Parallel()
-	if err := (Noop{}).Play(context.Background(), ""); err != nil {
-		t.Fatalf("expected nil error from Noop, got %v", err)
-	}
-	if err := (Noop{}).Play(context.Background(), "glass"); err != nil {
-		t.Fatalf("expected nil error from Noop with preset, got %v", err)
-	}
+	t.Run("Should return nil for empty sound", func(t *testing.T) {
+		t.Parallel()
+		if err := (Noop{}).Play(context.Background(), ""); err != nil {
+			t.Fatalf("expected nil error from Noop, got %v", err)
+		}
+	})
+	t.Run("Should return nil for preset name", func(t *testing.T) {
+		t.Parallel()
+		if err := (Noop{}).Play(context.Background(), "glass"); err != nil {
+			t.Fatalf("expected nil error from Noop with preset, got %v", err)
+		}
+	})
 }

As per coding guidelines: "MUST use t.Run("Should...") pattern for ALL test cases".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/sound/player_test.go` around lines 37 - 45, Update
TestNoop_Play_AlwaysNil to use table-driven subtests: create a slice of test
cases (with Name like "Should return nil for empty" and "Should return nil for
preset" or similar) containing the preset string values, call t.Run for each
case and run the (Noop{}).Play(context.Background(), preset) assertion inside
the subtest (optionally calling t.Parallel() inside each subtest), and keep the
original error checks but replace the inline checks with t.Fatalf inside each
t.Run; reference the existing test function TestNoop_Play_AlwaysNil and the
Noop.Play method when locating the change.

116-121: Consider removing or expanding this trivial constructor test.

Per coding guidelines, "Constructor tests that only check != nil" is listed as an anti-pattern to reject. This test doesn't verify any meaningful business logic.

If you want to keep coverage for New(), consider testing that the returned player actually behaves correctly (e.g., verify it's an osPlayer with proper wiring by calling Play with a known preset and checking the command resolution).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/sound/player_test.go` around lines 116 - 121, The
TestNew_ReturnsNonNilPlayer test is trivial and should be removed or replaced:
either delete the TestNew_ReturnsNonNilPlayer test entirely, or expand it to
assert behavior by calling New() and verifying the concrete type and wiring
(e.g., assert the returned value from New() is an *osPlayer or implements
expected methods) and exercise a method such as Play with a deterministic preset
to confirm command resolution and dependencies are set up correctly; update the
test to call New(), cast/inspect the returned player, and invoke Play with a
known input to assert expected outputs or mock interactions instead of only
checking for nil.
internal/core/sound/subscriber_test.go (1)

55-56: Remove unnecessary loop variable capture in table-driven tests.

The tc := tc pattern at lines 56 and 90 is no longer needed. Go 1.26.1 (the project's target version) captures loop variables per-iteration by default since Go 1.22. These lines can be safely removed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/sound/subscriber_test.go` around lines 55 - 56, Remove the
redundant per-iteration variable capture statement "tc := tc" inside the
table-driven test loops; locate the "for _, tc := range cases {" loops in the
test and delete the inner "tc := tc" lines (they are unnecessary as Go 1.26.1
already captures loop variables per iteration), leaving the loop body to
reference tc directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/core/model/runtime_config.go`:
- Around line 88-95: ApplyDefaults() currently only checks cfg.SoundOnCompleted
== "" and cfg.SoundOnFailed == "", so whitespace-only values bypass defaults;
update ApplyDefaults (the block gated by cfg.SoundEnabled) to treat
whitespace-only as unset by trimming with strings.TrimSpace (or equivalent)
before the emptiness check and then set
DefaultSoundOnCompleted/DefaultSoundOnFailed when trimmed value is empty; ensure
you reference cfg.SoundOnCompleted and cfg.SoundOnFailed and leave ErrEmptySound
behavior unchanged (it will still catch truly empty values later).

In `@internal/core/run/exec/exec.go`:
- Around line 946-978: The cancellation of the run context prevents sounds from
playing; in notifySoundForKind detach playback from the run cancellation by
creating a non-cancellable context (use context.WithoutCancel(ctx)) and then
wrap that in a short timeout (context.WithTimeout(..., e.g. 3*time.Second)) so
playback is bounded; pass the timeout context into sound.Notify. Update
notifySoundForKind (referenced from submitRuntimeEvent) to construct ctxNoCancel
and ctxTimeout, defer cancel(), and call sound.Notify(ctxTimeout, ...) and
ensure time is imported.

In `@internal/core/run/executor/execution.go`:
- Around line 282-290: notifySoundForKind currently passes the run context into
sound.Notify which may already be canceled; change it to create a short-lived,
non-cancelable derived context with a small timeout so sound playback runs even
if the parent ctx is canceled. In notifySoundForKind, wrap the incoming ctx with
context.WithoutCancel(ctx) and then context.WithTimeout(...) (choose a short
duration, e.g. ~1–3s), defer the timeout cancel, and pass that new ctx to
sound.Notify while keeping the existing cfg.SoundEnabled check and sound.Config
usage.

In `@internal/core/sound/player_windows.go`:
- Around line 15-21: In resolveWindowsCommand, the PowerShell single-quoted path
must escape any apostrophes by doubling them before interpolating into the
script; modify resolveWindowsCommand to call ResolvePath(sound) as before, then
replace every single-quote in the returned path with two single-quotes (e.g.,
strings.ReplaceAll(path, "'", "''")) and use that escapedPath in the
fmt.Sprintf("(New-Object Media.SoundPlayer '%s').PlaySync()", escapedPath) so
the returned command ("powershell", []string{"-NoProfile", "-Command", script},
nil) remains correct even for paths containing apostrophes.

In `@internal/core/sound/presets_test.go`:
- Around line 40-55: Update the test for the case "unknown preset produces
descriptive error" to assert the returned error message contains the unknown
preset string and (optionally) the list of known presets: after calling
ResolvePath(tc.input) in the failing branch, replace the generic nil-check with
an assertion like ErrorContains(err, tc.input) (and/or ErrorContains(err, "known
presets") or a sample preset name) so the test verifies the user-facing message
mentions the unknown value and helps detect regressions in ResolvePath's error
text.
- Around line 12-28: The test uses a hardcoded Unix absolute path in variable
absPath which breaks on Windows; replace the literal "/tmp/custom.aiff" with a
platform-portable path built from t.TempDir() and filepath.Join (e.g., absPath
:= filepath.Join(t.TempDir(), "custom.aiff")), ensure the test imports
"path/filepath" and calls t.TempDir() at test start, and keep the existing cases
(the trimming case and others) using that absPath so ResolvePath-related
assertions remain correct.

---

Nitpick comments:
In `@internal/core/sound/player_test.go`:
- Around line 37-45: Update TestNoop_Play_AlwaysNil to use table-driven
subtests: create a slice of test cases (with Name like "Should return nil for
empty" and "Should return nil for preset" or similar) containing the preset
string values, call t.Run for each case and run the
(Noop{}).Play(context.Background(), preset) assertion inside the subtest
(optionally calling t.Parallel() inside each subtest), and keep the original
error checks but replace the inline checks with t.Fatalf inside each t.Run;
reference the existing test function TestNoop_Play_AlwaysNil and the Noop.Play
method when locating the change.
- Around line 116-121: The TestNew_ReturnsNonNilPlayer test is trivial and
should be removed or replaced: either delete the TestNew_ReturnsNonNilPlayer
test entirely, or expand it to assert behavior by calling New() and verifying
the concrete type and wiring (e.g., assert the returned value from New() is an
*osPlayer or implements expected methods) and exercise a method such as Play
with a deterministic preset to confirm command resolution and dependencies are
set up correctly; update the test to call New(), cast/inspect the returned
player, and invoke Play with a known input to assert expected outputs or mock
interactions instead of only checking for nil.

In `@internal/core/sound/player.go`:
- Around line 61-64: The current pattern checks the error from p.runner.Run then
returns it, which is redundant; replace the if-block with a direct return of the
call — i.e., return p.runner.Run(ctx, name, args...) — to simplify the code in
the method that calls p.runner.Run.

In `@internal/core/sound/presets.go`:
- Line 18: Rename the truncated constant PresetSubmar to PresetSubmarine for
consistency; update its declaration and every reference (including in
KnownPresets() and all preset maps/variables that use PresetSubmar) to the new
identifier PresetSubmarine, ensuring the string value remains "submarine" and
run/rebuild to catch any missed usages.
- Around line 115-125: The windowsPresets map currently uses hardcoded
"C:\Windows\Media\..." paths which can break on machines where WINDIR is
different; update the code so windowsPresets is built at runtime using
os.Getenv("WINDIR") (fallback to "C:\\Windows" if empty) to join with "Media"
and the filenames (e.g., build the map in an init function or a helper like
newWindowsPresets) and keep the same keys (PresetGlass, PresetBasso, PresetPing,
PresetHero, PresetFunk, PresetTink, PresetSubmar) so callers are unchanged.
Ensure path construction uses filepath.Join on Windows.

In `@internal/core/sound/subscriber_test.go`:
- Around line 55-56: Remove the redundant per-iteration variable capture
statement "tc := tc" inside the table-driven test loops; locate the "for _, tc
:= range cases {" loops in the test and delete the inner "tc := tc" lines (they
are unnecessary as Go 1.26.1 already captures loop variables per iteration),
leaving the loop body to reference tc directly.

In `@internal/core/workspace/config_test.go`:
- Around line 649-710: Replace the three separate top-level tests
(TestLoadConfigParsesSoundSection, TestLoadConfigLeavesSoundDisabledByDefault,
TestLoadConfigRejectsEmptySoundPreset) with a single table-driven test (e.g.,
TestLoadConfig_SoundCases) that defines a slice of cases containing a
descriptive name, the workspace config string, expected error flag, and expected
Sound fields; iterate cases and call t.Run("Should ...", func(t *testing.T) {
... }) for each, create temp dir, writeWorkspaceConfig, call LoadConfig, and
assert expectations (cfg.Sound.Enabled, OnCompleted, OnFailed or error contents
like "sound.on_completed") inside each subtest so all scenarios are covered
uniformly and follow the required t.Run("Should...") pattern.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 16915609-7a9b-4869-89ef-73852560fc86

📥 Commits

Reviewing files that changed from the base of the PR and between 187427a and ded7f4a.

⛔ Files ignored due to path filters (1)
  • skills/compozy/references/config-reference.md is excluded by !**/*.md
📒 Files selected for processing (21)
  • internal/cli/state.go
  • internal/cli/workspace_config.go
  • internal/core/api.go
  • internal/core/model/model_test.go
  • internal/core/model/runtime_config.go
  • internal/core/run/exec/exec.go
  • internal/core/run/executor/execution.go
  • internal/core/run/internal/runshared/config.go
  • internal/core/sound/player.go
  • internal/core/sound/player_test.go
  • internal/core/sound/player_unix.go
  • internal/core/sound/player_windows.go
  • internal/core/sound/presets.go
  • internal/core/sound/presets_test.go
  • internal/core/sound/runner.go
  • internal/core/sound/subscriber.go
  • internal/core/sound/subscriber_test.go
  • internal/core/workspace/config_test.go
  • internal/core/workspace/config_types.go
  • internal/core/workspace/config_validate.go
  • sdk/extension/types.go

Comment thread internal/core/model/runtime_config.go
Comment on lines 946 to +978
func (s *execRunState) submitRuntimeEvent(kind eventspkg.EventKind, payload any) error {
if s == nil || s.journal == nil {
if s == nil {
return nil
}
ctx := s.ctx
if ctx == nil {
ctx = context.Background()
}
event, err := newRuntimeEvent(s.runArtifacts.RunID, kind, payload)
if err != nil {
return err
if s.journal != nil {
event, err := newRuntimeEvent(s.runArtifacts.RunID, kind, payload)
if err != nil {
return err
}
if err := s.journal.Submit(ctx, event); err != nil {
return err
}
}
s.notifySoundForKind(ctx, kind)
return nil
}

// notifySoundForKind plays the configured sound for a terminal lifecycle
// event kind. It runs synchronously so the audio finishes before the exec
// run state is closed. When the [sound] feature flag is off this is a no-op.
func (s *execRunState) notifySoundForKind(ctx context.Context, kind eventspkg.EventKind) {
if s == nil || s.cfg == nil || !s.cfg.SoundEnabled {
return
}
return s.journal.Submit(ctx, event)
sound.Notify(ctx, sound.Config{
Player: sound.New(),
OnCompleted: s.cfg.SoundOnCompleted,
OnFailed: s.cfg.SoundOnFailed,
}, kind, slog.Default())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and examine the sound package implementation
find . -path "*/internal/core/sound/*.go" -type f | head -20

Repository: compozy/compozy

Length of output: 384


🏁 Script executed:

# Also search for the submitRuntimeEvent function to understand the full context
rg "submitRuntimeEvent" -A 5 -B 5 --type go

Repository: compozy/compozy

Length of output: 14616


🏁 Script executed:

# Check the exec.go file around the flagged lines
wc -l internal/core/run/exec/exec.go

Repository: compozy/compozy

Length of output: 94


🏁 Script executed:

# Examine the sound/runner.go to see how context is used with exec.CommandContext
cat -n ./internal/core/sound/runner.go

Repository: compozy/compozy

Length of output: 868


🏁 Script executed:

# Check the exact implementation of submitRuntimeEvent in exec.go around lines 946-978
sed -n '945,980p' internal/core/run/exec/exec.go

Repository: compozy/compozy

Length of output: 1043


🏁 Script executed:

# Look at the sound.Notify function signature and implementation
cat -n ./internal/core/sound/player.go | head -100

Repository: compozy/compozy

Length of output: 2884


🏁 Script executed:

# Search for where context cancellation happens and when submitRuntimeEvent is called
rg "run.cancelled\|EventKindRunCancelled" -B 5 -A 5 --type go

Repository: compozy/compozy

Length of output: 41


🏁 Script executed:

# Look for context.WithoutCancel usage in the codebase to see if it's being used elsewhere
rg "WithoutCancel\|WithCancel" --type go

Repository: compozy/compozy

Length of output: 41


🏁 Script executed:

# Check what Go version is being targeted in go.mod
head -5 go.mod

Repository: compozy/compozy

Length of output: 114


🏁 Script executed:

# Find where emitTurnResult is called to understand the cancellation timing
rg "emitTurnResult" -B 3 -A 3 --type go

Repository: compozy/compozy

Length of output: 954


🏁 Script executed:

# Search for where s.ctx is set and how/when it might be cancelled
rg "\.ctx\s*=" --type go -A 2 | grep -A 2 "execRunState"

Repository: compozy/compozy

Length of output: 41


🏁 Script executed:

# Look for the context cancellation flow in the exec.go file
rg "cancel\|Done\|Context" internal/core/run/exec/exec.go | head -30

Repository: compozy/compozy

Length of output: 41


🏁 Script executed:

# Check how the state is created and context initialized
rg "execRunState\{" -A 10 --type go | head -40

Repository: compozy/compozy

Length of output: 2427


🏁 Script executed:

# Look at the complete flow of how terminal events are emitted
sed -n '800,900p' internal/core/run/exec/exec.go

Repository: compozy/compozy

Length of output: 2933


🏁 Script executed:

# Check when context cancellation occurs during the run execution
rg "ctx.Done\|select.*Done" internal/core/run/exec/exec.go -B 2 -A 2

Repository: compozy/compozy

Length of output: 41


🏁 Script executed:

# Look for where the run gets canceled and how the context relates to it
sed -n '1,100p' internal/core/run/exec/exec.go

Repository: compozy/compozy

Length of output: 3930


🏁 Script executed:

# Check the RunExec function to see how the context flows
rg "func RunExec" -A 30 internal/core/run/exec/exec.go

Repository: compozy/compozy

Length of output: 41


🏁 Script executed:

# Verify context.WithoutCancel is available in Go 1.26
web_search
Go context.WithoutCancel availability version

Repository: compozy/compozy

Length of output: 150


🌐 Web query:

Go context.WithoutCancel availability version 1.21 1.26

💡 Result:

context.WithoutCancel was added in Go 1.21 and is available in Go 1.26. It returns a derived context from the parent that ignores the parent's cancellation: the returned context has no Deadline, no Err, nil Done channel, and context.Cause returns nil. Prior to Go 1.21, it was not in the standard library; users implemented workarounds or waited for the official addition.

Citations:


🏁 Script executed:

# Search for how RunExec is called and understand the context lifecycle
rg "RunExec" -B 5 -A 10 --type go | head -50

Repository: compozy/compozy

Length of output: 41


🏁 Script executed:

# Check if there's any context timeout or cancellation mechanism already in place
rg "WithTimeout\|WithCancel\|WithDeadline" internal/core/run/exec/exec.go

Repository: compozy/compozy

Length of output: 41


🏁 Script executed:

# Look at the full context to understand when emitTurnResult is called relative to context cancellation
sed -n '550,700p' internal/core/run/exec/exec.go

Repository: compozy/compozy

Length of output: 4486


Detach playback from run cancellation to ensure sounds play on terminal states.

When a run is cancelled, submitRuntimeEvent forwards the already-done run context to sound.Notify, which eventually calls exec.CommandContext. A cancelled context immediately terminates the process, so the cancellation sound never plays. Use context.WithoutCancel to detach sound playback from the run's cancellation lifecycle, then add a short timeout to ensure bounded execution.

Suggested fix
 func (s *execRunState) submitRuntimeEvent(kind eventspkg.EventKind, payload any) error {
 	if s == nil {
 		return nil
 	}
 	ctx := s.ctx
 	if ctx == nil {
 		ctx = context.Background()
 	}
 	if s.journal != nil {
 		event, err := newRuntimeEvent(s.runArtifacts.RunID, kind, payload)
 		if err != nil {
 			return err
 		}
 		if err := s.journal.Submit(ctx, event); err != nil {
 			return err
 		}
 	}
-	s.notifySoundForKind(ctx, kind)
+	soundCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), time.Second)
+	defer cancel()
+	s.notifySoundForKind(soundCtx, kind)
 	return nil
 }
📝 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
func (s *execRunState) submitRuntimeEvent(kind eventspkg.EventKind, payload any) error {
if s == nil || s.journal == nil {
if s == nil {
return nil
}
ctx := s.ctx
if ctx == nil {
ctx = context.Background()
}
event, err := newRuntimeEvent(s.runArtifacts.RunID, kind, payload)
if err != nil {
return err
if s.journal != nil {
event, err := newRuntimeEvent(s.runArtifacts.RunID, kind, payload)
if err != nil {
return err
}
if err := s.journal.Submit(ctx, event); err != nil {
return err
}
}
s.notifySoundForKind(ctx, kind)
return nil
}
// notifySoundForKind plays the configured sound for a terminal lifecycle
// event kind. It runs synchronously so the audio finishes before the exec
// run state is closed. When the [sound] feature flag is off this is a no-op.
func (s *execRunState) notifySoundForKind(ctx context.Context, kind eventspkg.EventKind) {
if s == nil || s.cfg == nil || !s.cfg.SoundEnabled {
return
}
return s.journal.Submit(ctx, event)
sound.Notify(ctx, sound.Config{
Player: sound.New(),
OnCompleted: s.cfg.SoundOnCompleted,
OnFailed: s.cfg.SoundOnFailed,
}, kind, slog.Default())
func (s *execRunState) submitRuntimeEvent(kind eventspkg.EventKind, payload any) error {
if s == nil {
return nil
}
ctx := s.ctx
if ctx == nil {
ctx = context.Background()
}
if s.journal != nil {
event, err := newRuntimeEvent(s.runArtifacts.RunID, kind, payload)
if err != nil {
return err
}
if err := s.journal.Submit(ctx, event); err != nil {
return err
}
}
soundCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), time.Second)
defer cancel()
s.notifySoundForKind(soundCtx, kind)
return nil
}
// notifySoundForKind plays the configured sound for a terminal lifecycle
// event kind. It runs synchronously so the audio finishes before the exec
// run state is closed. When the [sound] feature flag is off this is a no-op.
func (s *execRunState) notifySoundForKind(ctx context.Context, kind eventspkg.EventKind) {
if s == nil || s.cfg == nil || !s.cfg.SoundEnabled {
return
}
sound.Notify(ctx, sound.Config{
Player: sound.New(),
OnCompleted: s.cfg.SoundOnCompleted,
OnFailed: s.cfg.SoundOnFailed,
}, kind, slog.Default())
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/run/exec/exec.go` around lines 946 - 978, The cancellation of
the run context prevents sounds from playing; in notifySoundForKind detach
playback from the run cancellation by creating a non-cancellable context (use
context.WithoutCancel(ctx)) and then wrap that in a short timeout
(context.WithTimeout(..., e.g. 3*time.Second)) so playback is bounded; pass the
timeout context into sound.Notify. Update notifySoundForKind (referenced from
submitRuntimeEvent) to construct ctxNoCancel and ctxTimeout, defer cancel(), and
call sound.Notify(ctxTimeout, ...) and ensure time is imported.

Comment on lines +282 to +290
func notifySoundForKind(ctx context.Context, cfg *config, kind events.EventKind, logger *slog.Logger) {
if cfg == nil || !cfg.SoundEnabled {
return
}
sound.Notify(ctx, sound.Config{
Player: sound.New(),
OnCompleted: cfg.SoundOnCompleted,
OnFailed: cfg.SoundOnFailed,
}, kind, logger)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Detach terminal sound playback from the run context.

By the time this helper runs, ctx is often already canceled on failed/cancelled shutdown paths, so the notification can be skipped for the very terminal outcomes this feature is meant to cover. Use a short-lived context.WithoutCancel(ctx) timeout here instead of the run context directly.

Suggested fix
func notifySoundForKind(ctx context.Context, cfg *config, kind events.EventKind, logger *slog.Logger) {
	if cfg == nil || !cfg.SoundEnabled {
		return
	}
-	sound.Notify(ctx, sound.Config{
+	notifyCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), time.Second)
+	defer cancel()
+	sound.Notify(notifyCtx, sound.Config{
		Player:      sound.New(),
		OnCompleted: cfg.SoundOnCompleted,
		OnFailed:    cfg.SoundOnFailed,
	}, kind, logger)
}
📝 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
func notifySoundForKind(ctx context.Context, cfg *config, kind events.EventKind, logger *slog.Logger) {
if cfg == nil || !cfg.SoundEnabled {
return
}
sound.Notify(ctx, sound.Config{
Player: sound.New(),
OnCompleted: cfg.SoundOnCompleted,
OnFailed: cfg.SoundOnFailed,
}, kind, logger)
func notifySoundForKind(ctx context.Context, cfg *config, kind events.EventKind, logger *slog.Logger) {
if cfg == nil || !cfg.SoundEnabled {
return
}
notifyCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), time.Second)
defer cancel()
sound.Notify(notifyCtx, sound.Config{
Player: sound.New(),
OnCompleted: cfg.SoundOnCompleted,
OnFailed: cfg.SoundOnFailed,
}, kind, logger)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/run/executor/execution.go` around lines 282 - 290,
notifySoundForKind currently passes the run context into sound.Notify which may
already be canceled; change it to create a short-lived, non-cancelable derived
context with a small timeout so sound playback runs even if the parent ctx is
canceled. In notifySoundForKind, wrap the incoming ctx with
context.WithoutCancel(ctx) and then context.WithTimeout(...) (choose a short
duration, e.g. ~1–3s), defer the timeout cancel, and pass that new ctx to
sound.Notify while keeping the existing cfg.SoundEnabled check and sound.Config
usage.

Comment thread internal/core/sound/player_windows.go
Comment thread internal/core/sound/presets_test.go Outdated
Comment thread internal/core/sound/presets_test.go Outdated
@pedronauck

Copy link
Copy Markdown
Member

@brunomguimaraes just fix the coderabbit review and we can merge it

Audio cues let operators step away from a long-running ACP session and
still know when the model finishes or fails. The feature is opt-in via a
new [sound] section in .compozy/config.toml and ships disabled, so it
costs nothing for users who do not want it.

Sound playback is synchronous at the emission point in both the workflow
executor and the exec pipeline (sound.Notify). A subscriber-goroutine
design was rejected because cleanup would race the terminal event and
silently drop the very sound it was meant to play. Synchronous playback
is single-flight, deterministic, and adds no concurrency surface.

Cross-platform commands are isolated behind build tags
(player_unix.go, player_windows.go) and the os/exec call is wrapped in a
commandRunner seam so tests never shell out. Presets resolve to native
file paths per OS (afplay on darwin, paplay on linux, powershell on
windows); absolute paths bypass preset lookup so users can ship custom
sounds.
@brunomguimaraes

Copy link
Copy Markdown
Contributor Author

@brunomguimaraes just fix the coderabbit review and we can merge it

Done @pedronauck , FYI the pre-existing TestHostAPITasksCreateRoundTripsMethodAndParams race in upstream sdk/extension deserves a separate fix PR regardless of what CodeRabbit says about this, it's unrelated to the sound work. I went ahead and created #104

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
internal/core/sound/presets_test.go (1)

130-134: Strengthen Windows preset assertion to exact-path validation.

The Windows case currently passes on any path containing "windows", which can mask regressions. Set wantPath explicitly (C:\Windows\Media\ding.wav) and assert exact equality, same as the Darwin/Linux cases.

Also applies to: 163-165

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/sound/presets_test.go` around lines 130 - 134, The Windows
preset test case for PresetPing should assert the exact resolved path instead of
checking for any "windows" substring: add a wantPath field with the literal
"C:\\Windows\\Media\\ding.wav" to the test case (the one using preset PresetPing
and goos "windows") and change the test assertion to compare equality against
wantPath (same exact equality behavior used by the Darwin/Linux cases); apply
the same change to the second Windows case referenced around lines 163-165 so
both Windows entries validate exact path equality rather than a substring.
internal/core/sound/subscriber_test.go (1)

67-68: Remove redundant loop variable captures.

Since Go 1.22+, loop variables are scoped per iteration, making tc := tc unnecessary. The project uses Go 1.26 per go.mod.

♻️ Suggested cleanup
 	for _, tc := range cases {
-		tc := tc
 		t.Run(tc.name, func(t *testing.T) {

Apply similarly at line 102.

Also applies to: 101-102

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/sound/subscriber_test.go` around lines 67 - 68, The test
contains redundant loop-variable shadowing like "tc := tc" inside the for-range
over "cases" (seen in the test loops in subscriber_test.go); remove those
unnecessary reassignments (both occurrences, the one around the first for _, tc
:= range cases and the similar second loop noted) so the code relies on Go 1.22+
per-iteration scoping and no longer creates shadowed "tc" variables.
internal/core/sound/player.go (1)

61-64: Simplify error return.

The runner error handling can be simplified to a single return statement.

♻️ Suggested simplification
-	if err := p.runner.Run(ctx, name, args...); err != nil {
-		return err
-	}
-	return nil
+	return p.runner.Run(ctx, name, args...)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/sound/player.go` around lines 61 - 64, The error handling in
the method can be simplified by returning the runner call directly instead of
using an if-check; replace the current if err := p.runner.Run(ctx, name,
args...); err != nil { return err } return nil with a single return
p.runner.Run(ctx, name, args...) to simplify code in the method where
p.runner.Run is invoked.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/core/sound/presets_test.go`:
- Line 36: Subtest names use tc.name and "input "+input which break the
repository rule requiring t.Run("Should...") pattern; update each t.Run
invocation in internal/core/sound/presets_test.go (the calls using tc.name and
the ones that build names from input) to use descriptive "Should ..." titles
(e.g., t.Run("Should return X when Y", func...) ) so every subtest follows the
t.Run("Should...") naming convention — change the t.Run arguments where they
reference tc.name or concatenate "input "+input (and the other occurrences
noted) to explicit "Should..." strings that describe the expected behavior.

---

Nitpick comments:
In `@internal/core/sound/player.go`:
- Around line 61-64: The error handling in the method can be simplified by
returning the runner call directly instead of using an if-check; replace the
current if err := p.runner.Run(ctx, name, args...); err != nil { return err }
return nil with a single return p.runner.Run(ctx, name, args...) to simplify
code in the method where p.runner.Run is invoked.

In `@internal/core/sound/presets_test.go`:
- Around line 130-134: The Windows preset test case for PresetPing should assert
the exact resolved path instead of checking for any "windows" substring: add a
wantPath field with the literal "C:\\Windows\\Media\\ding.wav" to the test case
(the one using preset PresetPing and goos "windows") and change the test
assertion to compare equality against wantPath (same exact equality behavior
used by the Darwin/Linux cases); apply the same change to the second Windows
case referenced around lines 163-165 so both Windows entries validate exact path
equality rather than a substring.

In `@internal/core/sound/subscriber_test.go`:
- Around line 67-68: The test contains redundant loop-variable shadowing like
"tc := tc" inside the for-range over "cases" (seen in the test loops in
subscriber_test.go); remove those unnecessary reassignments (both occurrences,
the one around the first for _, tc := range cases and the similar second loop
noted) so the code relies on Go 1.22+ per-iteration scoping and no longer
creates shadowed "tc" variables.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c3d192df-466f-45d6-819d-b77542a51d43

📥 Commits

Reviewing files that changed from the base of the PR and between ded7f4a and 7a4329e.

⛔ Files ignored due to path filters (1)
  • skills/compozy/references/config-reference.md is excluded by !**/*.md
📒 Files selected for processing (21)
  • internal/cli/state.go
  • internal/cli/workspace_config.go
  • internal/core/api.go
  • internal/core/model/model_test.go
  • internal/core/model/runtime_config.go
  • internal/core/run/exec/exec.go
  • internal/core/run/executor/execution.go
  • internal/core/run/internal/runshared/config.go
  • internal/core/sound/player.go
  • internal/core/sound/player_test.go
  • internal/core/sound/player_unix.go
  • internal/core/sound/player_windows.go
  • internal/core/sound/presets.go
  • internal/core/sound/presets_test.go
  • internal/core/sound/runner.go
  • internal/core/sound/subscriber.go
  • internal/core/sound/subscriber_test.go
  • internal/core/workspace/config_test.go
  • internal/core/workspace/config_types.go
  • internal/core/workspace/config_validate.go
  • sdk/extension/types.go
✅ Files skipped from review due to trivial changes (1)
  • internal/core/run/internal/runshared/config.go
🚧 Files skipped from review as they are similar to previous changes (13)
  • sdk/extension/types.go
  • internal/core/workspace/config_validate.go
  • internal/core/sound/player_windows.go
  • internal/core/workspace/config_types.go
  • internal/core/workspace/config_test.go
  • internal/core/sound/player_test.go
  • internal/core/model/runtime_config.go
  • internal/core/sound/runner.go
  • internal/core/sound/player_unix.go
  • internal/core/model/model_test.go
  • internal/core/sound/subscriber.go
  • internal/core/api.go
  • internal/core/sound/presets.go


for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use t.Run("Should...") naming for subtests to match repository test policy.

Current subtest names (tc.name, "input "+input) don’t follow the required Should... pattern. Please rename case titles/subtest names accordingly for consistency.

Suggested update
- name:    "absolute path is returned verbatim",
+ name:    "ShouldReturnAbsolutePathVerbatim",

- name:    "trims surrounding whitespace on absolute path",
+ name:    "ShouldTrimSurroundingWhitespaceOnAbsolutePath",

- t.Run("input "+input, func(t *testing.T) {
+ t.Run("ShouldReturnErrEmptySoundForWhitespaceOnlyInput", func(t *testing.T) {

- name:   "unknown preset returns false",
+ name:   "ShouldReturnFalseForUnknownPreset",

As per coding guidelines, "MUST use t.Run("Should...") pattern for ALL test cases".

Also applies to: 54-54, 151-151, 218-218

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/sound/presets_test.go` at line 36, Subtest names use tc.name
and "input "+input which break the repository rule requiring t.Run("Should...")
pattern; update each t.Run invocation in internal/core/sound/presets_test.go
(the calls using tc.name and the ones that build names from input) to use
descriptive "Should ..." titles (e.g., t.Run("Should return X when Y", func...)
) so every subtest follows the t.Run("Should...") naming convention — change the
t.Run arguments where they reference tc.name or concatenate "input "+input (and
the other occurrences noted) to explicit "Should..." strings that describe the
expected behavior.

- Rename PresetSubmar to PresetSubmarine for naming consistency with
  every other preset constant. Internal identifier only; the TOML value
  stays "submarine" so user configs do not break.
- Resolve Windows preset paths via os.Getenv("WINDIR") with a
  C:\Windows fallback instead of hardcoding the install directory, so
  non-standard Windows layouts work without users having to fall back
  to absolute paths. Split into windowsPresetFiles (basenames) and
  windowsPresetPath helper so both branches can be unit-tested from
  any host OS.
- Simplify osPlayer.Play to return p.runner.Run(...) directly instead
  of wrapping it in an err-check that only re-returns.
- Fold the three TestLoadConfig sound tests into a single table-driven
  TestLoadConfigSoundSection per coding guidelines, and add a fourth
  case covering the whitespace-only on_failed rejection branch.
- Convert TestNoop_Play_AlwaysNil to table-driven with Should-style
  subtest names so it follows the same pattern as the rest of the file.
- Replace the trivial TestNew_ReturnsNonNilPlayer constructor
  nil-check with TestNew_WiresPlatformPlayer, which exercises the
  real resolver and asserts that darwin gets afplay, linux gets
  paplay, and unsupported platforms fall back to Noop.
- Drop the now-redundant tc := tc loop variable captures; Go 1.22+
  captures per-iteration automatically and this project is on 1.26.
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