From 4fbbca6a99dfd80cca293edd9762c6206b1d93a5 Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Fri, 7 Aug 2026 15:30:33 +0800 Subject: [PATCH] refactor(go): adopt modern stdlib idioms and gate them (#7831) * refactor(go): adopt modern stdlib idioms across kernel and desktop Mechanical sweep with golangci-lint's modernize/intrange/copyloopvar/ usestdlibvars fixers plus hand-fixes for what they could not rewrite: slices.Contains, min/max, slices.Backward, WaitGroup.Go, reflect.TypeFor, maps.Copy, strings.Cut/CutPrefix/SplitSeq, strings.Builder, range-over-int. Net -549 lines with no behavior change. Three loops in session.go stay index-based: slices.Backward yields element copies, so the fixer's rewrite silently dropped writes to DecisionReceipts and ToolCalls (caught by TestDelivery* and TestDependentSameBatch*). All 49 slices.Backward sites were audited for that shape; only these three write through the index, and they carry a nolint with the reason. CaptureAfter's scan loop had an empty body and never ran any logic, so the fixer could not express it. Drop it and the accessor it was the only caller of. omitzero is deliberately untouched: turning omitempty into omitzero changes the JSON written for nested structs, which is a wire-format change to session files and the extension protocol. * build(lint): gate the modern-idiom linters Nothing stopped the codebase from drifting back to pre-1.22 idioms. Both modules are now clean for modernize, intrange, copyloopvar, and usestdlibvars, so enable them rather than leaving the sweep as a one-off. omitzero stays disabled with the reason inline; usetesting, perfsprint, and errorlint still have residue and land separately. --- .golangci.yml | 10 ++ benchmarks/context-maintenance-e2e/main.go | 6 +- cmd/e2ebench/diff.go | 37 +++--- cmd/e2ebench/main.go | 3 +- cmd/e2ebench/mutation.go | 2 +- cmd/e2ebench/swebench.go | 31 ++--- cmd/e2ebench/swebench_run.go | 4 +- desktop/app.go | 31 ++--- desktop/app_autosave_test.go | 12 +- desktop/app_session_dedup_test.go | 2 +- desktop/app_test.go | 80 ++++++------- desktop/bot_bridge_test.go | 2 +- desktop/bot_connection_app.go | 2 +- desktop/bound_array_contract_test.go | 4 +- desktop/checkpoints_cancode_test.go | 2 +- desktop/cmd/update-helper/main_windows.go | 2 +- desktop/crash_app.go | 2 +- desktop/crash_pending_test.go | 8 +- desktop/deferred_rebuild.go | 5 +- desktop/devinfo.go | 6 +- desktop/export_test.go | 2 - desktop/external_opener_darwin.go | 2 +- desktop/hang_watchdog.go | 10 +- desktop/hang_watchdog_windows.go | 5 +- desktop/hang_watchdog_windows_test.go | 2 +- desktop/heartbeat.go | 12 +- desktop/heartbeat_test.go | 4 +- desktop/history_perf_test.go | 6 +- desktop/hooks_settings_app.go | 8 +- desktop/main_test.go | 2 +- desktop/metrics_app.go | 9 +- desktop/race_regression_test.go | 16 +-- desktop/recovery_gc_test.go | 4 +- desktop/reload_runtime_test.go | 11 +- desktop/remote_app_test.go | 2 +- desktop/remote_hosts.go | 2 +- desktop/remote_hosts_test.go | 6 +- desktop/remote_lifecycle_test.go | 6 +- desktop/remote_window.go | 4 +- desktop/remote_window_test.go | 3 +- desktop/runtime_rebuilt_event_test.go | 4 +- desktop/session_prompt_test.go | 21 ++-- desktop/session_recovery_cleanup_test.go | 2 +- desktop/sessions.go | 2 +- desktop/sessions_test.go | 10 +- desktop/settings_app.go | 13 +-- desktop/shared_host.go | 4 +- desktop/tab_event_sink_test.go | 10 +- desktop/tabs.go | 72 +++++------- desktop/tabs_display_buffer_test.go | 4 +- desktop/tabs_order_test.go | 12 +- desktop/tabs_runtime_status_test.go | 6 +- desktop/tabs_sink_context_test.go | 2 +- desktop/tabs_topic_test.go | 22 ++-- desktop/task_monitor_test.go | 8 +- desktop/temphelper_test.go | 2 +- desktop/terminal_test.go | 6 +- desktop/theme_image.go | 8 +- desktop/theme_pack.go | 7 +- desktop/theme_pack_test.go | 4 +- desktop/theme_plugin_test.go | 4 +- desktop/tray_loop_windows_test.go | 2 +- desktop/updater_deb_linux.go | 2 +- desktop/updater_install_linux.go | 2 +- desktop/updater_mac.go | 4 +- desktop/updater_test.go | 36 +++--- desktop/window_state_test.go | 2 +- desktop/workspace_changes.go | 2 +- desktop/workspace_test.go | 4 +- internal/acp/protocol_test.go | 2 +- internal/acp/server.go | 12 +- internal/acp/service.go | 5 +- internal/acp/service_lock_test.go | 2 +- internal/acp/status.go | 7 +- internal/agent/agent.go | 11 +- internal/agent/branch.go | 5 +- internal/agent/cache_shape.go | 4 +- internal/agent/cachehit_e2e_test.go | 10 +- internal/agent/cancel_test.go | 4 +- internal/agent/compact_loop_e2e_test.go | 2 +- internal/agent/coordinator.go | 25 ++-- internal/agent/coordinator_test.go | 22 ++-- internal/agent/empty_final_test.go | 2 +- internal/agent/fleet.go | 6 +- internal/agent/fleet_test.go | 2 +- internal/agent/goal_display.go | 6 +- internal/agent/interrupted_recovery.go | 5 +- internal/agent/loop_e2e_test.go | 4 +- internal/agent/parallel_tasks.go | 13 +-- internal/agent/parallel_tasks_test.go | 6 +- internal/agent/prune_test.go | 6 +- internal/agent/reasoning_language.go | 6 +- internal/agent/reasoning_warn_state.go | 8 +- internal/agent/reasoning_warn_state_test.go | 12 +- internal/agent/recovery_gc.go | 4 +- internal/agent/repeat_guard_test.go | 4 +- internal/agent/run_loop.go | 13 +-- internal/agent/save.go | 15 ++- internal/agent/save_test.go | 2 +- internal/agent/scheduler_test.go | 8 +- internal/agent/session.go | 3 + internal/agent/session_concurrency_test.go | 18 ++- internal/agent/session_events_test.go | 6 +- internal/agent/session_lease_test.go | 6 +- internal/agent/session_meta_ledger_test.go | 20 ++-- internal/agent/storm_test.go | 10 +- internal/agent/subagent_progress.go | 9 +- internal/agent/subagent_progress_test.go | 4 +- internal/agent/subagent_result.go | 21 ++-- internal/agent/task.go | 10 +- internal/agent/task_test.go | 8 +- internal/agent/textsink.go | 2 +- internal/agent/todo_progress_guard_test.go | 6 +- internal/agent/usecapability.go | 9 +- internal/agent/usecapability_test.go | 4 +- internal/agent/width.go | 2 +- internal/agent/write_claims.go | 2 +- internal/autoresearch/bounded_store_test.go | 4 +- internal/autoresearch/store.go | 13 +-- internal/autoresearch/store_test.go | 14 +-- internal/boot/boot.go | 17 +-- internal/boot/boot_test.go | 106 ++++++++--------- internal/boot/extension_dispatch_test.go | 2 +- internal/boot/extension_firstboot_test.go | 5 +- internal/boot/extension_provider_test.go | 5 +- internal/boot/extension_sidecar_test.go | 2 +- internal/boot/prompt_stability_test.go | 20 +--- internal/boot/resolver.go | 4 +- internal/boot/temphelper_test.go | 2 +- internal/bot/feishu/feishu.go | 19 ++- internal/bot/feishu/feishu_test.go | 2 +- internal/bot/gateway.go | 14 +-- internal/bot/gateway_lock_test.go | 4 +- internal/bot/gateway_test.go | 6 +- internal/bot/pairing.go | 2 +- internal/bot/pairing_test.go | 4 +- internal/bot/project_index.go | 17 ++- internal/bot/qq/adapter.go | 6 +- internal/bot/qq/adapter_stop_test.go | 12 +- internal/bot/qq/gateway.go | 8 +- internal/bot/render_test.go | 2 +- internal/bot/session.go | 7 +- internal/bot/turn_dispatch_test.go | 3 +- internal/bot/types.go | 15 +-- internal/bot/weixin/weixin.go | 24 ++-- internal/capability/capability.go | 5 +- internal/capability/capability_test.go | 2 +- internal/capability/semantic.go | 2 +- internal/checkpoint/checkpoint.go | 15 +-- internal/checkpoint/checkpoint_test.go | 2 +- internal/checkpoint/transaction.go | 7 +- internal/cli/bot_test.go | 8 +- internal/cli/box.go | 5 +- internal/cli/chat_ghost_test.go | 2 +- internal/cli/chat_render_test.go | 4 +- internal/cli/chat_tui.go | 86 +++++--------- internal/cli/chat_tui_paste.go | 18 ++- internal/cli/chat_tui_test.go | 36 +++--- internal/cli/cli.go | 12 +- internal/cli/cli_flags.go | 2 +- internal/cli/complete.go | 43 ++----- internal/cli/complete_test.go | 2 +- internal/cli/composer_selection.go | 7 +- internal/cli/consecutive_tool_markers_test.go | 2 +- internal/cli/diffview.go | 12 +- internal/cli/diffview_test.go | 2 +- internal/cli/extension_surface.go | 4 +- internal/cli/gitstatus.go | 4 +- internal/cli/machine_identity_test.go | 8 +- internal/cli/mcp_manager.go | 5 +- internal/cli/mcp_manager_view.go | 5 +- internal/cli/mcp_test.go | 6 +- internal/cli/mcp_view.go | 20 +--- internal/cli/md.go | 27 ++--- internal/cli/mouse_reenable.go | 5 +- internal/cli/quick_picker.go | 5 +- internal/cli/quick_picker_test.go | 2 +- internal/cli/remote_cmd_test.go | 2 +- internal/cli/resume_test.go | 4 +- internal/cli/run_metrics.go | 9 +- internal/cli/run_metrics_partial_test.go | 20 ++-- internal/cli/scroll_state_test.go | 20 ++-- internal/cli/select.go | 15 +-- internal/cli/select_test.go | 2 +- internal/cli/setup_manager.go | 2 +- internal/cli/shell_completion.go | 18 +-- internal/cli/shell_completion_test.go | 8 +- internal/cli/skill_hooks.go | 8 +- internal/cli/skill_picker_test.go | 2 +- internal/cli/skill_picker_view.go | 5 +- internal/cli/slash_catalog_test.go | 6 +- internal/cli/slash_registry.go | 7 +- internal/cli/status_footer.go | 2 +- internal/cli/subagent.go | 2 +- internal/cli/theme.go | 10 +- internal/cli/toolcard.go | 7 +- internal/cli/transcript.go | 22 +--- internal/cli/transcript_test.go | 22 ++-- internal/cli/tui_diagnostics.go | 6 +- internal/cli/tui_diagnostics_test.go | 4 +- internal/cli/upgrade.go | 6 +- internal/cli/upgrade_test.go | 6 +- internal/cli/width_test.go | 4 +- internal/cli/wrap_cache.go | 2 +- internal/command/inspect.go | 5 +- internal/command/slashtool_test.go | 2 +- internal/config/backfill_test.go | 9 +- internal/config/config.go | 8 +- internal/config/credentials_keyring_unix.go | 7 +- internal/config/dotenv.go | 2 +- internal/config/edit.go | 8 +- internal/config/effort.go | 8 +- internal/config/fetch.go | 9 +- internal/config/load.go | 18 ++- internal/config/mcp_activation_test.go | 6 +- internal/config/migrate.go | 9 +- internal/config/mutate.go | 13 ++- internal/config/mutate_test.go | 2 +- internal/config/path_access.go | 5 +- internal/config/paths.go | 5 +- internal/config/remote_credentials.go | 5 +- internal/config/render_test.go | 6 +- internal/config/system_prompt_test.go | 2 +- internal/config/vision.go | 7 +- internal/control/admission_test.go | 12 +- internal/control/attachments.go | 8 +- internal/control/autoresearch_manager.go | 2 +- internal/control/branches.go | 5 +- internal/control/controller.go | 52 ++++----- internal/control/controller_lock_test.go | 8 +- internal/control/controller_test.go | 51 ++++---- internal/control/goal_concurrency_test.go | 6 +- internal/control/goal_runtime_test.go | 8 +- internal/control/goal_test.go | 2 +- internal/control/imagecompress.go | 10 +- internal/control/imagecompress_test.go | 4 +- internal/control/mcp.go | 8 +- internal/control/memory_command.go | 4 +- internal/control/memory_test.go | 6 +- internal/control/plan_seed_test.go | 9 +- internal/control/plan_todos.go | 7 +- internal/control/planner_gate.go | 14 +-- internal/control/recovery.go | 7 +- internal/control/refs.go | 4 +- internal/control/turn_orchestrator_test.go | 2 +- internal/crashreport/cli.go | 6 +- internal/crashreport/cli_test.go | 6 +- internal/desktoplauncher/launcher.go | 2 +- internal/diff/diff_extra_test.go | 2 +- internal/diff/largediff_test.go | 4 +- internal/doctor/session_redact.go | 4 +- internal/doctor/skill_health.go | 4 +- internal/environment/probe.go | 4 +- internal/environment/probe_test.go | 2 +- internal/event/event_test.go | 8 +- internal/eventwire/wire.go | 2 +- internal/eventwire/wire_test.go | 4 +- internal/evidence/evidence.go | 88 ++++---------- internal/evidence/review_report.go | 5 +- internal/evidence/risk.go | 10 +- internal/extension/benchmark_test.go | 6 +- internal/extension/builder.go | 5 +- internal/extension/builder_test.go | 2 +- internal/extension/dispatch/benchmark_test.go | 7 +- internal/extension/dispatch/dispatch_test.go | 5 +- internal/extension/protocol/dto_test.go | 4 +- internal/extension/protocol/dto_ui.go | 10 +- internal/extension/protocol/registry.go | 2 +- internal/extension/protocol/schema.go | 6 +- internal/extension/protocol/validate.go | 50 ++++---- internal/extension/protocolgen/sdkgo.go | 16 +-- .../extension/providerext/providerext_test.go | 9 +- internal/extension/providerext/stream_test.go | 2 +- internal/extension/replace.go | 5 +- .../rpcwire/conn_strict_regression_test.go | 10 +- internal/extension/rpcwire/conn_test.go | 9 +- internal/extension/rpcwire/stall_test.go | 7 +- internal/extension/runtimeset.go | 5 +- internal/extension/sidecar/benchmark_test.go | 7 +- internal/extension/sidecar/client.go | 13 +-- internal/extension/sidecar/content.go | 5 +- internal/extension/sidecar/content_test.go | 4 +- internal/extension/sidecar/externalize.go | 2 +- internal/extension/sidecar/fake_test.go | 4 +- internal/extension/sidecar/manager_test.go | 2 +- internal/extension/sidecar/process.go | 5 +- internal/extension/snapshot.go | 5 +- internal/extension/uihub/hub_test.go | 2 +- internal/fileref/search.go | 7 +- internal/fileutil/encoding/encoding.go | 2 +- internal/fileutil/globset.go | 4 +- internal/guardian/guardian.go | 7 +- internal/guardian/guardian_test.go | 13 ++- internal/guardian/transcript.go | 18 +-- internal/history/search.go | 10 +- internal/history/search_test.go | 2 +- internal/hook/hook.go | 15 +-- internal/hook/inspect.go | 3 +- internal/hook/windows_compat.go | 6 +- internal/i18n/catalog_parity_test.go | 2 +- internal/i18n/i18n_test.go | 6 +- internal/installlayout/activate.go | 5 +- internal/installlayout/current.go | 2 +- internal/installsource/apply.go | 8 +- internal/installsource/plan.go | 4 +- internal/installsource/plugin_package.go | 4 +- internal/instruction/resolver.go | 2 +- internal/jobs/concurrency_stress_test.go | 4 +- internal/jobs/jobs.go | 7 +- internal/jobs/jobs_test.go | 6 +- internal/lsp/position.go | 6 +- internal/mcplaunch/launch.go | 2 +- internal/memory/auto_recall.go | 7 +- internal/memory/recall_test.go | 2 +- internal/memory/store.go | 6 +- internal/memory/store_v2.go | 5 +- internal/memory/store_v2_test.go | 6 +- internal/netclient/netclient_test.go | 40 +++---- internal/outputstyle/outputstyle.go | 9 +- internal/permission/bash_readonly.go | 7 +- internal/plugin/cache_test.go | 2 +- internal/plugin/chrome_devtools_live_test.go | 3 +- internal/plugin/codegraph_limit_test.go | 2 +- internal/plugin/host_concurrency_test.go | 6 +- internal/plugin/hotadd_test.go | 2 +- internal/plugin/known_overrides.go | 13 +-- internal/plugin/launcher_lock.go | 4 +- internal/plugin/launcher_lock_test.go | 8 +- internal/plugin/lazy_test.go | 18 ++- internal/plugin/plugin.go | 27 ++--- internal/plugin/plugin_test.go | 11 +- internal/plugin/process_mode_test.go | 4 +- internal/plugin/protocol_inbound.go | 4 +- internal/plugin/proxy_e2e_test.go | 6 +- internal/plugin/startup.go | 5 +- internal/plugin/stats.go | 14 +-- internal/plugin/stats_test.go | 12 +- internal/plugin/stdio_cancel_test.go | 2 +- internal/plugin/toolresult_image_test.go | 2 +- internal/plugin/transport_http.go | 5 +- internal/plugin/transport_sse.go | 5 +- internal/plugin/transport_sse_test.go | 2 +- internal/plugin/transport_stdio.go | 13 ++- internal/pluginpkg/claude_compat.go | 4 +- internal/pluginpkg/manifest_v1.go | 14 +-- internal/pluginpkg/state_lock_test.go | 12 +- internal/productdocs/docs_test.go | 2 +- internal/provider/anthropic/anthropic_test.go | 3 +- internal/provider/openai/host.go | 7 +- internal/provider/openai/openai.go | 5 +- internal/provider/openai/openai_test.go | 5 +- internal/provider/openai/think.go | 5 +- internal/provider/provider.go | 15 +-- internal/provider/responses/responses.go | 5 +- internal/provider/responses/responses_test.go | 14 +-- internal/provider/retry.go | 5 +- internal/provider/retry_test.go | 8 +- internal/recovery/decision_test.go | 4 +- internal/recovery/gate_test.go | 32 ++--- internal/recovery/persist_test.go | 2 +- internal/recovery/reviewer.go | 2 +- internal/recovery/reviewer_test.go | 2 +- internal/recovery/state.go | 11 +- internal/releaseasset/cli.go | 2 +- internal/remote/auth.go | 8 +- internal/remote/bootstrap/bootstrap.go | 4 +- internal/remote/bootstrap/platform.go | 2 +- internal/remote/dial.go | 5 +- internal/remote/reconnect_test.go | 10 +- internal/remote/remote.go | 2 +- internal/remote/sftpfs/detect.go | 11 +- internal/remote/sshtest/sshtest.go | 6 +- internal/repair/config_test.go | 4 +- internal/repair/mutation_lock.go | 15 +-- internal/repair/transaction.go | 5 +- internal/repair/update.go | 6 +- internal/repair/update_legacy.go | 6 +- internal/retrieval/bm25.go | 10 +- internal/sandbox/seatbelt_darwin_test.go | 24 +--- internal/sandbox/seatbelt_other.go | 2 +- internal/sandbox/seatbelt_other_test.go | 8 +- internal/sandbox/session_temp_env.go | 2 +- internal/secrets/redact_test.go | 12 +- internal/serve/auth.go | 12 +- internal/serve/auth_test.go | 42 +++---- internal/serve/broadcaster_test.go | 2 +- internal/serve/modelswitch_test.go | 16 +-- internal/serve/serve_lock_test.go | 3 +- internal/serve/serve_portfile_test.go | 2 +- internal/serve/serve_test.go | 8 +- internal/sessiontemp/manager_test.go | 10 +- internal/shellparse/bash.go | 4 +- internal/shellrun/runner_test.go | 2 +- internal/shellsafe/bash_redirect.go | 2 +- internal/skill/builtins_test.go | 9 +- internal/skill/skill.go | 17 +-- internal/skill/skill_test.go | 9 +- internal/skill/tools.go | 2 +- internal/stats/query.go | 5 +- internal/stats/stats_test.go | 4 +- internal/sysproxy/sysproxy.go | 6 +- internal/taskintent/heuristic.go | 24 ++-- internal/taskintent/intent.go | 4 +- internal/taskmonitor/control.go | 2 +- internal/taskmonitor/control_test.go | 15 +-- internal/taskmonitor/jsonstore.go | 8 +- internal/taskmonitor/recorder.go | 6 +- internal/telemetry/client_test.go | 2 +- internal/telemetry/sink.go | 7 +- internal/tool/builtin/bash.go | 13 ++- .../tool/builtin/bash_cancel_windows_test.go | 6 +- internal/tool/builtin/bash_reap_test.go | 2 +- .../tool/builtin/bash_sandbox_escape_test.go | 6 +- internal/tool/builtin/bgjobs.go | 2 +- internal/tool/builtin/encoding_helpers.go | 12 +- internal/tool/builtin/gitignore_e2e_test.go | 2 +- internal/tool/builtin/managed_config.go | 8 +- internal/tool/builtin/post_write_receipt.go | 8 +- .../tool/builtin/post_write_receipt_test.go | 2 +- internal/tool/builtin/readfile_stream_test.go | 4 +- internal/tool/builtin/tool_extra_test.go | 2 +- internal/tool/registry_canon_test.go | 2 +- internal/tool/sessiontool/sessiontool.go | 12 +- internal/tool/sessiontool/sessiontool_test.go | 4 +- internal/tool/tool.go | 8 +- internal/worktree/worktree.go | 2 +- tools/repolint/baseline.json | 109 +++++++++--------- tools/repolint/comments_test.go | 14 ++- 428 files changed, 1558 insertions(+), 2248 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 6410390731..b271c82c10 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -2,13 +2,23 @@ version: "2" linters: enable: + - copyloopvar - errcheck - errorlint - govet - ineffassign + - intrange + - modernize - staticcheck - unused + - usestdlibvars settings: + modernize: + # omitempty -> omitzero changes which fields are written for nested + # structs. That is a wire-format change to session files and the + # extension protocol, not a refactor, so it lands on its own. + disable: + - omitzero errcheck: check-type-assertions: false exclude-functions: diff --git a/benchmarks/context-maintenance-e2e/main.go b/benchmarks/context-maintenance-e2e/main.go index fda902a80e..ac553392bc 100644 --- a/benchmarks/context-maintenance-e2e/main.go +++ b/benchmarks/context-maintenance-e2e/main.go @@ -55,7 +55,7 @@ func fakeGoFile(nonce string, i, size int) string { func seedSession(nonce string) *agent.Session { s := agent.NewSession("You are a terse coding agent reviewing a Go codebase.") s.Add(provider.Message{Role: provider.RoleUser, Content: "Review every file in module " + nonce + " one by one. Keep notes short."}) - for i := 0; i < fatResults; i++ { + for i := range fatResults { id := fmt.Sprintf("c%02d", i) name := fmt.Sprintf("src/file%02d.go", i) s.Add(provider.Message{Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{ID: id, Name: "read_file", Arguments: fmt.Sprintf(`{"path":%q}`, name)}}}) @@ -180,7 +180,7 @@ func resume(dir string) { func comprehension(trials int) { p := prov() pass := 0 - for t := 0; t < trials; t++ { + for t := range trials { dir, err := os.MkdirTemp("", "cm-e2e-") if err != nil { fmt.Fprintln(os.Stderr, err) @@ -198,7 +198,7 @@ func comprehension(trials int) { s.Add(provider.Message{Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{ID: "r1", Name: "read_file", Arguments: `{"path":"config.go"}`}}}) s.Add(provider.Message{Role: provider.RoleTool, ToolCallID: "r1", Name: "read_file", Content: content}) s.Add(provider.Message{Role: provider.RoleAssistant, Content: "Noted the constants in config.go."}) - for i := 0; i < 4; i++ { + for i := range 4 { s.Add(provider.Message{Role: provider.RoleUser, Content: fmt.Sprintf("ack %d", i)}) s.Add(provider.Message{Role: provider.RoleAssistant, Content: "ok"}) } diff --git a/cmd/e2ebench/diff.go b/cmd/e2ebench/diff.go index 3cf6664d31..b608719939 100644 --- a/cmd/e2ebench/diff.go +++ b/cmd/e2ebench/diff.go @@ -48,10 +48,7 @@ func runDiff(o diffOpts) string { pkgs := packagesOf(srcFiles) prompt := buildDiffPrompt(srcFiles, pkgs, truncate(gitOut(o.repo, "diff", o.base+"...HEAD", "--"))) - attempts := o.attempts - if attempts < 1 { - attempts = 1 - } + attempts := max(o.attempts, 1) var best diffReport made := 0 for i := 1; i <= attempts; i++ { @@ -406,7 +403,7 @@ func parseCoverProfile(repo, path string) map[string][]coverBlock { return nil } out := map[string][]coverBlock{} - for _, ln := range strings.Split(string(data), "\n") { + for ln := range strings.SplitSeq(string(data), "\n") { if ln == "" || strings.HasPrefix(ln, "mode:") { continue } @@ -433,8 +430,8 @@ func repoRelFromModulePath(p string) string { if strings.HasPrefix(p, prefix) { return p[len(prefix):] } - if i := strings.IndexByte(p, '/'); i >= 0 { - return p[i+1:] + if _, after, ok := strings.Cut(p, "/"); ok { + return after } return p } @@ -447,7 +444,7 @@ func changedLineSet(repo, base string, srcFiles []string) map[string]map[int]boo out := map[string]map[int]bool{} file := "" newLine := 0 - for _, ln := range strings.Split(diff, "\n") { + for ln := range strings.SplitSeq(diff, "\n") { // '-' (deletion) lines are intentionally unhandled: they don't advance the // new-side line counter, so they fall through with no case. switch { @@ -457,10 +454,10 @@ func changedLineSet(repo, base string, srcFiles []string) map[string]map[int]boo case strings.HasPrefix(ln, "@@"): // @@ -a,b +c,d @@ — start collecting at new-side line c. // Digit-only cut: malformed headers (e.g. `@@ +abc @@`) fail closed. - if plus := strings.Index(ln, "+"); plus >= 0 { - num := ln[plus+1:] + if _, after, ok := strings.Cut(ln, "+"); ok { + num := after end := len(num) - for i := 0; i < len(num); i++ { + for i := range len(num) { if num[i] < '0' || num[i] > '9' { end = i break @@ -503,9 +500,9 @@ func countAssertionPins(ps []pinResult) int { func parseNewTests(diff string) []testRef { var refs []testRef pkg := "" - for _, ln := range strings.Split(diff, "\n") { - if strings.HasPrefix(ln, "+++ b/") { - pkg = "./" + filepath.ToSlash(filepath.Dir(strings.TrimPrefix(ln, "+++ b/"))) + for ln := range strings.SplitSeq(diff, "\n") { + if after, ok := strings.CutPrefix(ln, "+++ b/"); ok { + pkg = "./" + filepath.ToSlash(filepath.Dir(after)) continue } if !strings.HasPrefix(ln, "+") || strings.HasPrefix(ln, "+++") { @@ -519,11 +516,11 @@ func parseNewTests(diff string) []testRef { // Method form `(r T) Name(...)` starts with '('; parse the receiver out before the name. var name string if sig[0] == '(' { - close := strings.IndexByte(sig, ')') - if close < 0 { + _, after, ok := strings.Cut(sig, ")") + if !ok { continue } - rest := strings.TrimSpace(sig[close+1:]) + rest := strings.TrimSpace(after) methodParen := strings.IndexByte(rest, '(') if methodParen <= 0 { continue @@ -545,7 +542,7 @@ func parseNewTests(diff string) []testRef { func countAdded(diff string) int { n := 0 - for _, ln := range strings.Split(diff, "\n") { + for ln := range strings.SplitSeq(diff, "\n") { if strings.HasPrefix(ln, "+") && !strings.HasPrefix(ln, "+++") { n++ } @@ -557,7 +554,7 @@ func countAdded(diff string) int { func failingTestNames(out string) []string { var names []string seen := map[string]bool{} - for _, ln := range strings.Split(out, "\n") { + for ln := range strings.SplitSeq(out, "\n") { ln = strings.TrimSpace(ln) if !strings.HasPrefix(ln, "--- FAIL:") { continue @@ -603,7 +600,7 @@ func changedGoFilesWorktree(repo string, includeTests bool) []string { func filterGo(out string, includeTests bool) []string { var keep []string - for _, f := range strings.Fields(strings.ReplaceAll(out, "\n", " ")) { + for f := range strings.FieldsSeq(strings.ReplaceAll(out, "\n", " ")) { if strings.HasSuffix(f, "_test.go") && !includeTests { continue } diff --git a/cmd/e2ebench/main.go b/cmd/e2ebench/main.go index 5854ed1901..0d553bea50 100644 --- a/cmd/e2ebench/main.go +++ b/cmd/e2ebench/main.go @@ -11,6 +11,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "sort" "strings" "time" @@ -526,7 +527,7 @@ func median(ms []int64) int64 { return 0 } sorted := append([]int64(nil), ms...) - sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] }) + slices.Sort(sorted) return sorted[len(sorted)/2] } diff --git a/cmd/e2ebench/mutation.go b/cmd/e2ebench/mutation.go index 5a214ccd73..0d0847f7c1 100644 --- a/cmd/e2ebench/mutation.go +++ b/cmd/e2ebench/mutation.go @@ -129,7 +129,7 @@ func mutantBody(fset *token.FileSet, results *ast.FieldList) string { if n == 0 { n = 1 } - for i := 0; i < n; i++ { + for range n { rets = append(rets, t) } } diff --git a/cmd/e2ebench/swebench.go b/cmd/e2ebench/swebench.go index e7fa288373..03c34aad9e 100644 --- a/cmd/e2ebench/swebench.go +++ b/cmd/e2ebench/swebench.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "fmt" + "slices" "strings" "reasonix/internal/ablation" @@ -137,30 +138,20 @@ func swebenchReportPath(model, runID string) string { // taxonomy. An id the grader never mentions is reported as unknown rather than // silently counted as unsolved. func (r swebenchReport) gradedClass(instanceID string) string { - for _, id := range r.ResolvedIDs { - if id == instanceID { - return "solved" - } + if slices.Contains(r.ResolvedIDs, instanceID) { + return "solved" } - for _, id := range r.EmptyPatchIDs { - if id == instanceID { - return "no_patch" - } + if slices.Contains(r.EmptyPatchIDs, instanceID) { + return "no_patch" } - for _, id := range r.ErrorIDs { - if id == instanceID { - return "grader_error" - } + if slices.Contains(r.ErrorIDs, instanceID) { + return "grader_error" } - for _, id := range r.IncompleteIDs { - if id == instanceID { - return "eval_timeout" - } + if slices.Contains(r.IncompleteIDs, instanceID) { + return "eval_timeout" } - for _, id := range r.UnresolvedIDs { - if id == instanceID { - return "wrong_patch" - } + if slices.Contains(r.UnresolvedIDs, instanceID) { + return "wrong_patch" } return "ungraded" } diff --git a/cmd/e2ebench/swebench_run.go b/cmd/e2ebench/swebench_run.go index f76061fb69..a50da15471 100644 --- a/cmd/e2ebench/swebench_run.go +++ b/cmd/e2ebench/swebench_run.go @@ -296,8 +296,8 @@ func proxyEnv(url string) []string { } func firstLine(s string) string { - if i := strings.IndexByte(s, '\n'); i >= 0 { - return strings.TrimSpace(s[:i]) + if before, _, ok := strings.Cut(s, "\n"); ok { + return strings.TrimSpace(before) } return strings.TrimSpace(s) } diff --git a/desktop/app.go b/desktop/app.go index cf3dea3c79..779861c2e1 100644 --- a/desktop/app.go +++ b/desktop/app.go @@ -12,6 +12,7 @@ import ( "fmt" "io" "log/slog" + "maps" "mime" "net/http" "net/url" @@ -20,6 +21,7 @@ import ( "path/filepath" "regexp" goruntime "runtime" + "slices" "sort" "strconv" "strings" @@ -2635,6 +2637,7 @@ func (a *App) CheckpointsForTab(tabID string) []CheckpointMeta { canCodeAfter := true codeFileSet := make(map[string]bool, len(metas)*2) codeFilePreview := []string{} + //nolint:modernize // slices.Backward yields element copies; this body writes through the index. for i := len(out) - 1; i >= 0; i-- { if len(out[i].Files) > 0 { hasCodeAfter = true @@ -5681,10 +5684,7 @@ func historyPageFromMessages(messages []HistoryMessage, beforeTurn, limit int) H if beforeTurn <= 0 || beforeTurn > totalTurns { beforeTurn = totalTurns } - startTurn := beforeTurn - limit - if startTurn < 0 { - startTurn = 0 - } + startTurn := max(beforeTurn-limit, 0) page := HistoryPage{ StartTurn: startTurn, EndTurn: beforeTurn, @@ -6033,10 +6033,7 @@ func historyPageFromProviderMessages( if beforeTurn <= 0 || beforeTurn > totalTurns { beforeTurn = totalTurns } - startTurn := beforeTurn - limit - if startTurn < 0 { - startTurn = 0 - } + startTurn := max(beforeTurn-limit, 0) page := HistoryPage{ StartTurn: startTurn, EndTurn: beforeTurn, @@ -6323,7 +6320,7 @@ func historyLineCount(s string) int { func historyNonEmptyLineCount(s string) int { count := 0 - for _, line := range strings.Split(s, "\n") { + for line := range strings.SplitSeq(s, "\n") { if strings.TrimSpace(line) != "" { count++ } @@ -6622,9 +6619,9 @@ func updateHistoryToolCallSummary(out []HistoryMessage, callID, output string) { if callID == "" { return } - for i := len(out) - 1; i >= 0; i-- { - for j := range out[i].ToolCalls { - call := &out[i].ToolCalls[j] + for _, v := range slices.Backward(out) { + for j := range v.ToolCalls { + call := &v.ToolCalls[j] if call.ID != callID { continue } @@ -8160,9 +8157,7 @@ func (a *App) mcpServersView() []ServerView { } ctrl := tab.Ctrl disabled := make(map[string]ServerView, len(tab.disabledMCP)) - for name, s := range tab.disabledMCP { - disabled[name] = s - } + maps.Copy(disabled, tab.disabledMCP) order := append([]string(nil), tab.mcpOrder...) workspaceRoot := tab.WorkspaceRoot tabID := tab.ID @@ -9447,9 +9442,7 @@ func cloneStringIntMap(values map[string]int) map[string]int { return nil } out := make(map[string]int, len(values)) - for key, value := range values { - out[key] = value - } + maps.Copy(out, values) return out } @@ -11327,7 +11320,7 @@ func saveExclusiveExportPayloads(targets []string, payloadCount int, payloadAt f // while bytes are staged; the caller restores finalMode only after the payload // has been completely written and synced. func createExportTempFile(dir string) (*os.File, os.FileMode, error) { - for attempt := 0; attempt < exportTempCreateAttempts; attempt++ { + for range exportTempCreateAttempts { var suffix [12]byte if _, err := rand.Read(suffix[:]); err != nil { return nil, 0, fmt.Errorf("generate export temp name: %w", err) diff --git a/desktop/app_autosave_test.go b/desktop/app_autosave_test.go index 8fe0aa632d..9b7dfe9bfe 100644 --- a/desktop/app_autosave_test.go +++ b/desktop/app_autosave_test.go @@ -124,12 +124,10 @@ func TestScheduleSnapshotCoalesces(t *testing.T) { _ = a var wg sync.WaitGroup - for i := 0; i < 64; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range 64 { + wg.Go(func() { tab.sink.Emit(event.Event{Kind: event.TurnDone}) - }() + }) } wg.Wait() @@ -335,11 +333,11 @@ func TestDesktopSnapshotConflictRecoveryRequiresRecoveryLease(t *testing.T) { detachedSessions: map[string]*WorkspaceTab{}, activeTabID: "recovery_tab", } - app.runtimeEvents.emit = func(ctx context.Context, name string, payload ...interface{}) { + app.runtimeEvents.emit = func(ctx context.Context, name string, payload ...any) { runtimeEvents <- runtimeEventEnvelope{ ctx: ctx, name: name, - payload: append([]interface{}(nil), payload...), + payload: append([]any(nil), payload...), } } tab := &WorkspaceTab{ diff --git a/desktop/app_session_dedup_test.go b/desktop/app_session_dedup_test.go index 1b4e96d010..8612b0f3d5 100644 --- a/desktop/app_session_dedup_test.go +++ b/desktop/app_session_dedup_test.go @@ -35,7 +35,7 @@ func TestCarriedRebuildsKeepOneSession(t *testing.T) { t.Fatal(err) } - for i := 0; i < 5; i++ { + for range 5 { prevPath := ctrl.SessionPath() carried := ctrl.History() ctrl.Close() diff --git a/desktop/app_test.go b/desktop/app_test.go index 5717d693e8..128b29c2b6 100644 --- a/desktop/app_test.go +++ b/desktop/app_test.go @@ -13,6 +13,7 @@ import ( "os/exec" "path/filepath" "reflect" + "slices" "strconv" "strings" "sync" @@ -1058,14 +1059,14 @@ func TestBackgroundRestorePlanAvoidsNormalWindowFlash(t *testing.T) { func TestEmitReadyInvokesReadyHook(t *testing.T) { app := NewApp() - var calls int32 + var calls atomic.Int32 app.readyHook = func() { - atomic.AddInt32(&calls, 1) + calls.Add(1) } app.emitReady(context.TODO()) - if got := atomic.LoadInt32(&calls); got != 1 { + if got := calls.Load(); got != 1 { t.Fatalf("ready hook calls = %d, want 1", got) } } @@ -1227,7 +1228,7 @@ func BenchmarkDesktopSettingsPayloads(b *testing.B) { b.Setenv("SHARED_PROVIDER_KEY", "sk-test") cfg := config.LoadForEdit(config.UserConfigPath()) - for i := 0; i < 40; i++ { + for i := range 40 { cfg.Providers = append(cfg.Providers, config.ProviderEntry{ Name: fmt.Sprintf("custom-%02d", i), Kind: "openai", @@ -1243,12 +1244,12 @@ func BenchmarkDesktopSettingsPayloads(b *testing.B) { app := NewApp() b.Run("Settings", func(b *testing.B) { - for i := 0; i < b.N; i++ { + for range b.N { _ = app.Settings() } }) b.Run("DesktopStartupSettings", func(b *testing.B) { - for i := 0; i < b.N; i++ { + for range b.N { _ = app.DesktopStartupSettings() } }) @@ -2073,7 +2074,6 @@ func TestResetProviderPresetAccessRejectsMissingSameNameProvider(t *testing.T) { func TestAddEveryProviderPresetAccessInstallsTemplate(t *testing.T) { for _, preset := range config.CuratedProviderPresets() { - preset := preset t.Run(preset.ID, func(t *testing.T) { isolateDesktopUserDirs(t) @@ -2890,7 +2890,7 @@ func TestSetEffortForTabReanchorsDepthCapRecoveryBranch(t *testing.T) { app := NewApp() app.ctx = context.Background() - app.runtimeEvents.emit = func(context.Context, string, ...interface{}) {} + app.runtimeEvents.emit = func(context.Context, string, ...any) {} tab := &WorkspaceTab{ ID: "tab_depth_cap_effort", Scope: "global", @@ -3566,7 +3566,7 @@ func TestSetModelForTabContinuesRecoveryPathAfterSnapshotConflict(t *testing.T) app := NewApp() app.ctx = context.Background() - app.runtimeEvents.emit = func(context.Context, string, ...interface{}) {} + app.runtimeEvents.emit = func(context.Context, string, ...any) {} tab := &WorkspaceTab{ ID: "tab_recovery_model", Scope: "global", @@ -4339,16 +4339,13 @@ func runQuickClickWorkspaceReconcileTest(t *testing.T, layoutStyle string) { errs := make(chan error, len(actions)) var wg sync.WaitGroup for _, action := range actions { - action := action - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { ready <- struct{}{} <-start if err := action.run(); err != nil { errs <- fmt.Errorf("%s: %w", action.name, err) } - }() + }) } for range actions { <-ready @@ -6793,7 +6790,7 @@ func assertSingleTeardownTimeoutNotice(t *testing.T, notices <-chan event.Event, t.Fatal("missing background-job teardown timeout notice") } var waited time.Duration - for _, field := range strings.Fields(notice.Detail) { + for field := range strings.FieldsSeq(notice.Detail) { if !strings.HasPrefix(field, "waited=") { continue } @@ -7297,7 +7294,7 @@ func BenchmarkDesktopListSessionsScoped(b *testing.B) { if err := os.MkdirAll(dir, 0o755); err != nil { b.Fatalf("mkdir %s: %v", dir, err) } - for i := 0; i < 120; i++ { + for i := range 120 { path := filepath.Join(dir, fmt.Sprintf("session-%03d.jsonl", i)) body := fmt.Sprintf(`{"role":"user","content":"session %03d"}`+"\n", i) if err := os.WriteFile(path, []byte(body), 0o644); err != nil { @@ -7312,7 +7309,7 @@ func BenchmarkDesktopListSessionsScoped(b *testing.B) { b.ReportAllocs() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { sessions := app.ListSessions() if len(sessions) != 120 { b.Fatalf("ListSessions len = %d, want 120", len(sessions)) @@ -7563,12 +7560,12 @@ func TestDesktopSharedHostProjectMCPConnectsWithoutLaunchApproval(t *testing.T) srv := desktopMCPHTTPServer(t) defer srv.Close() - if err := os.WriteFile(filepath.Join(dir, "reasonix.toml"), []byte(fmt.Sprintf(` + if err := os.WriteFile(filepath.Join(dir, "reasonix.toml"), fmt.Appendf(nil, ` [[plugins]] name = "h" type = "http" url = %q -`, srv.URL)), 0o644); err != nil { +`, srv.URL), 0o644); err != nil { t.Fatal(err) } @@ -7759,12 +7756,12 @@ func TestSetMCPServerEnabledSharedHostPreservesSiblingTabs(t *testing.T) { srv := desktopMCPHTTPServer(t) defer srv.Close() - if err := os.WriteFile(filepath.Join(dir, "reasonix.toml"), []byte(fmt.Sprintf(` + if err := os.WriteFile(filepath.Join(dir, "reasonix.toml"), fmt.Appendf(nil, ` [[plugins]] name = "h" type = "http" url = %q -`, srv.URL)), 0o644); err != nil { +`, srv.URL), 0o644); err != nil { t.Fatal(err) } @@ -7909,12 +7906,12 @@ func TestReconnectMCPServerUsesEffectiveProjectConfigWhenUserNameIsShadowed(t *t if err := userCfg.SaveTo(config.UserConfigPath()); err != nil { t.Fatal(err) } - if err := os.WriteFile(filepath.Join(dir, "reasonix.toml"), []byte(fmt.Sprintf(` + if err := os.WriteFile(filepath.Join(dir, "reasonix.toml"), fmt.Appendf(nil, ` [[plugins]] name = "h" type = "http" url = %q -`, projectServer.URL)), 0o644); err != nil { +`, projectServer.URL), 0o644); err != nil { t.Fatal(err) } @@ -8055,7 +8052,7 @@ func newGatedDesktopMCPLaunchFixture(t *testing.T, startGateAddr string) gatedDe gateConfig = fmt.Sprintf("DESKTOP_MCP_START_GATE_ADDR = %q\n", startGateAddr) } helperArgs := []string{"-test.run=TestDesktopMCPHelperProcess", "--"} - if err := os.WriteFile(filepath.Join(dir, "reasonix.toml"), []byte(fmt.Sprintf(` + if err := os.WriteFile(filepath.Join(dir, "reasonix.toml"), fmt.Appendf(nil, ` [[plugins]] name = "h" command = %q @@ -8067,7 +8064,7 @@ DESKTOP_MCP_SINGLE_INSTANCE_ADDR = %q %s [sandbox] network = true -`, exe, singleInstanceAddr, gateConfig)), 0o644); err != nil { +`, exe, singleInstanceAddr, gateConfig), 0o644); err != nil { t.Fatal(err) } @@ -8305,13 +8302,13 @@ func installGatedTestPluginPackage(t *testing.T, mcpServerName string) string { if err := os.MkdirAll(root, 0o755); err != nil { t.Fatal(err) } - if err := os.WriteFile(filepath.Join(root, pluginpkg.NativeManifest), []byte(fmt.Sprintf(`{ + if err := os.WriteFile(filepath.Join(root, pluginpkg.NativeManifest), fmt.Appendf(nil, `{ "name": "review-helper", "version": "1.0.0", "mcpServers": { %q: { "type": "stdio", "command": "helper" } } -}`, mcpServerName)), 0o644); err != nil { +}`, mcpServerName), 0o644); err != nil { t.Fatal(err) } if err := pluginpkg.Upsert(reasonixHome, pluginpkg.InstalledPlugin{ @@ -9082,13 +9079,13 @@ func TestRemoveMCPServerRejectsPluginManagedServerWithoutDisconnecting(t *testin if err := os.MkdirAll(root, 0o755); err != nil { t.Fatal(err) } - if err := os.WriteFile(filepath.Join(root, pluginpkg.NativeManifest), []byte(fmt.Sprintf(`{ + if err := os.WriteFile(filepath.Join(root, pluginpkg.NativeManifest), fmt.Appendf(nil, `{ "name": "superpowers", "version": "1.0.0", "mcpServers": { "helper": { "type": "http", "url": %q } } -}`, srv.URL)), 0o644); err != nil { +}`, srv.URL), 0o644); err != nil { t.Fatal(err) } if err := pluginpkg.Upsert(reasonixHome, pluginpkg.InstalledPlugin{ @@ -10127,7 +10124,7 @@ func TestRunShellForTabStaysBoundDuringRapidProjectTabSwitching(t *testing.T) { waitForShellDispatch(t, shellEvents, marker) waitForFile(t, filepath.Join(projectA, marker), "shell") - for i := 0; i < 8; i++ { + for range 8 { if err := app.SetActiveTab("project-b"); err != nil { t.Fatalf("SetActiveTab(project-b): %v", err) } @@ -10281,12 +10278,7 @@ func newBackgroundJobController(t *testing.T, label string) *control.Controller } func hasLevel(levels []string, want string) bool { - for _, level := range levels { - if level == want { - return true - } - } - return false + return slices.Contains(levels, want) } func hasCommand(cmds []CommandInfo, name string) bool { @@ -10616,14 +10608,14 @@ func TestScanPromptHistoryFromDirUsesSessionActivityBeforeEventInterleaving(t *t early := filepath.Join(dir, "early.jsonl") late := filepath.Join(dir, "late.jsonl") - if err := os.WriteFile(early, []byte(fmt.Sprintf(`{"role":"user","content":"early first","time":%d} + if err := os.WriteFile(early, fmt.Appendf(nil, `{"role":"user","content":"early first","time":%d} {"role":"assistant","content":"ok"} {"role":"user","content":"early second","time":%d} -`, base.UnixMilli(), base.Add(time.Minute).UnixMilli())), 0o644); err != nil { +`, base.UnixMilli(), base.Add(time.Minute).UnixMilli()), 0o644); err != nil { t.Fatal(err) } - if err := os.WriteFile(late, []byte(fmt.Sprintf(`{"role":"user","content":"late newest","time":%d} -`, base.Add(2*time.Minute).UnixMilli())), 0o644); err != nil { + if err := os.WriteFile(late, fmt.Appendf(nil, `{"role":"user","content":"late newest","time":%d} +`, base.Add(2*time.Minute).UnixMilli()), 0o644); err != nil { t.Fatal(err) } // Invert file mtimes: session activity should keep each session grouped @@ -10792,8 +10784,8 @@ func TestScanPromptHistoryPaginatesCurrentSessionBeforeCrossSession(t *testing.T other := filepath.Join(dir, "other.jsonl") var lines []byte for i := range 55 { - lines = append(lines, []byte(fmt.Sprintf(`{"role":"user","content":"current %d"} -`, i))...) + lines = append(lines, fmt.Appendf(nil, `{"role":"user","content":"current %d"} +`, i)...) } if err := os.WriteFile(current, lines, 0o644); err != nil { t.Fatal(err) @@ -10867,8 +10859,8 @@ func TestScanPromptHistoryFromDirReadsAllEntriesForInternalHelper(t *testing.T) dir := t.TempDir() var lines []byte for i := range 250 { - lines = append(lines, []byte(fmt.Sprintf(`{"role":"user","content":"prompt %d"} -`, i))...) + lines = append(lines, fmt.Appendf(nil, `{"role":"user","content":"prompt %d"} +`, i)...) } if err := os.WriteFile(filepath.Join(dir, "many.jsonl"), lines, 0o644); err != nil { t.Fatal(err) diff --git a/desktop/bot_bridge_test.go b/desktop/bot_bridge_test.go index 4935fe969e..36840ab068 100644 --- a/desktop/bot_bridge_test.go +++ b/desktop/bot_bridge_test.go @@ -155,7 +155,7 @@ func TestBridgeTakeoverSwitchAnnouncesReleaseToOldTab(t *testing.T) { t.Fatalf("switch to B: %v", err) } seen := map[string]bool{} - for i := 0; i < 2; i++ { + for range 2 { select { case got := <-env.announced: seen[got[0]] = true diff --git a/desktop/bot_connection_app.go b/desktop/bot_connection_app.go index c664b6ce96..c9032d814f 100644 --- a/desktop/bot_connection_app.go +++ b/desktop/bot_connection_app.go @@ -693,7 +693,7 @@ func postFeishuInstallFormResult(base string, body map[string]string) (map[strin for k, v := range body { reqBody.Set(k, v) } - req, err := http.NewRequestWithContext(ctx, "POST", strings.TrimRight(base, "/")+"/oauth/v1/app/registration", strings.NewReader(reqBody.Encode())) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(base, "/")+"/oauth/v1/app/registration", strings.NewReader(reqBody.Encode())) if err != nil { return nil, 0, err } diff --git a/desktop/bound_array_contract_test.go b/desktop/bound_array_contract_test.go index d5b1f43cb1..c479b3d20d 100644 --- a/desktop/bound_array_contract_test.go +++ b/desktop/bound_array_contract_test.go @@ -114,12 +114,12 @@ func assertRequiredJSONSlicesNonNil(t *testing.T, path string, value reflect.Val if value.Kind() == reflect.Slice && value.IsNil() { t.Fatalf("%s is a nil slice; JSON contract requires []", path) } - for i := 0; i < value.Len(); i++ { + for i := range value.Len() { assertRequiredJSONSlicesNonNil(t, path, value.Index(i)) } case reflect.Struct: typ := value.Type() - for i := 0; i < value.NumField(); i++ { + for i := range value.NumField() { fieldType := typ.Field(i) if fieldType.PkgPath != "" { continue diff --git a/desktop/checkpoints_cancode_test.go b/desktop/checkpoints_cancode_test.go index 9353101c9f..e936aa3a5b 100644 --- a/desktop/checkpoints_cancode_test.go +++ b/desktop/checkpoints_cancode_test.go @@ -230,7 +230,7 @@ func TestCheckpointsForTabLimitsCumulativeFilePreview(t *testing.T) { } content := "old" files := make([]checkpoint.FileSnap, 0, checkpointFilePreviewLimit+5) - for i := 0; i < checkpointFilePreviewLimit+5; i++ { + for i := range checkpointFilePreviewLimit + 5 { files = append(files, checkpoint.FileSnap{Path: "file-" + strconv.Itoa(1000+i) + ".txt", Content: &content}) } now := time.Now() diff --git a/desktop/cmd/update-helper/main_windows.go b/desktop/cmd/update-helper/main_windows.go index c803661e34..f378baae7f 100644 --- a/desktop/cmd/update-helper/main_windows.go +++ b/desktop/cmd/update-helper/main_windows.go @@ -534,7 +534,7 @@ func cleanupOwnedWindowsUpdateDirectory(path string, owner os.FileInfo) error { if path == "" || owner == nil || !owner.IsDir() { return fmt.Errorf("Windows update cleanup identity is incomplete") } - for attempt := 0; attempt < 16; attempt++ { + for attempt := range 16 { cleanup := fmt.Sprintf("%s.reasonix-cleanup-%d-%d", path, time.Now().UTC().UnixNano(), attempt) from, err := windows.UTF16PtrFromString(path) if err != nil { diff --git a/desktop/crash_app.go b/desktop/crash_app.go index 043cf60c82..18b5f5ca5a 100644 --- a/desktop/crash_app.go +++ b/desktop/crash_app.go @@ -155,7 +155,7 @@ func baseCrashReport(kind string) crashReport { } func topFrameFromStack(stack string) string { - for _, line := range strings.Split(stack, "\n") { + for line := range strings.SplitSeq(stack, "\n") { line = strings.TrimSpace(line) if line == "" { continue diff --git a/desktop/crash_pending_test.go b/desktop/crash_pending_test.go index f82cb92c77..7f9cb35f3e 100644 --- a/desktop/crash_pending_test.go +++ b/desktop/crash_pending_test.go @@ -107,11 +107,9 @@ func TestWritePendingReportQueueIsBoundedUnderConcurrentWriters(t *testing.T) { var done sync.WaitGroup var successes atomic.Int32 - for i := 0; i < writers; i++ { + for range writers { ready.Add(1) - done.Add(1) - go func() { - defer done.Done() + done.Go(func() { report := baseCrashReport("performance") report.Source = "native.watchdog" report.Label = "mac.main_thread.hang" @@ -121,7 +119,7 @@ func TestWritePendingReportQueueIsBoundedUnderConcurrentWriters(t *testing.T) { if writePendingReport(report, false) { successes.Add(1) } - }() + }) } ready.Wait() close(start) diff --git a/desktop/deferred_rebuild.go b/desktop/deferred_rebuild.go index 94fe9649d0..41de8e5002 100644 --- a/desktop/deferred_rebuild.go +++ b/desktop/deferred_rebuild.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "log/slog" + "maps" "strings" "sync" "time" @@ -163,9 +164,7 @@ func (a *App) deferredRebuildTick(markIdle bool) bool { return true } pending := make(map[string]string, len(d.pending)) - for id, setting := range d.pending { - pending[id] = setting - } + maps.Copy(pending, d.pending) d.mu.Unlock() for tabID, setting := range pending { diff --git a/desktop/devinfo.go b/desktop/devinfo.go index 945967ea02..8277a6517d 100644 --- a/desktop/devinfo.go +++ b/desktop/devinfo.go @@ -28,7 +28,7 @@ func collectDeviceInfo() deviceInfo { } func parseCPUModel(cpuinfo string) string { - for _, line := range strings.Split(cpuinfo, "\n") { + for line := range strings.SplitSeq(cpuinfo, "\n") { if name, ok := strings.CutPrefix(line, "model name"); ok { if _, v, ok := strings.Cut(name, ":"); ok { return strings.TrimSpace(v) @@ -39,7 +39,7 @@ func parseCPUModel(cpuinfo string) string { } func parseMemTotalBytes(meminfo string) uint64 { - for _, line := range strings.Split(meminfo, "\n") { + for line := range strings.SplitSeq(meminfo, "\n") { if rest, ok := strings.CutPrefix(line, "MemTotal:"); ok { kb, err := strconv.ParseUint(strings.TrimSuffix(strings.TrimSpace(rest), " kB"), 10, 64) if err != nil { @@ -52,7 +52,7 @@ func parseMemTotalBytes(meminfo string) uint64 { } func parseOSReleasePrettyName(osRelease string) string { - for _, line := range strings.Split(osRelease, "\n") { + for line := range strings.SplitSeq(osRelease, "\n") { if v, ok := strings.CutPrefix(line, "PRETTY_NAME="); ok { return strings.Trim(strings.TrimSpace(v), `"`) } diff --git a/desktop/export_test.go b/desktop/export_test.go index 4dff32465c..93a311977a 100644 --- a/desktop/export_test.go +++ b/desktop/export_test.go @@ -81,7 +81,6 @@ func TestExportErrorsDoNotExposeSelectedDirectory(t *testing.T) { }, } for _, test := range tests { - test := test t.Run(test.name, func(t *testing.T) { err := test.run(test.path) if err == nil { @@ -290,7 +289,6 @@ func TestConcurrentMultipartExportsHaveSingleCompleteWinner(t *testing.T) { var ready sync.WaitGroup ready.Add(len(batches)) for _, batch := range batches { - batch := batch go func() { ready.Done() <-start diff --git a/desktop/external_opener_darwin.go b/desktop/external_opener_darwin.go index de991a26d2..7ab19986ee 100644 --- a/desktop/external_opener_darwin.go +++ b/desktop/external_opener_darwin.go @@ -48,7 +48,7 @@ func darwinInstalledApplicationIndex() map[string]string { defer cancel() out, err := exec.CommandContext(ctx, "/usr/bin/mdfind", "kMDItemContentType == 'com.apple.application-bundle'").Output() if err == nil { - for _, line := range strings.Split(string(out), "\n") { + for line := range strings.SplitSeq(string(out), "\n") { add(line) } } diff --git a/desktop/hang_watchdog.go b/desktop/hang_watchdog.go index 6e0eaf11e1..ea5563fb71 100644 --- a/desktop/hang_watchdog.go +++ b/desktop/hang_watchdog.go @@ -24,10 +24,7 @@ var ( ) func recordMainThreadHeartbeat(t time.Time) { - elapsed := t.Sub(mainThreadClockBase) - if elapsed < 0 { - elapsed = 0 - } + elapsed := max(t.Sub(mainThreadClockBase), 0) mainThreadLastHeartbeatElapsed.Store(int64(elapsed)) mainThreadLastHeartbeatWall.Store(t.UnixNano()) } @@ -38,10 +35,7 @@ func mainThreadHeartbeatAge(now time.Time) (time.Duration, time.Time, bool) { if lastWall <= 0 { return 0, time.Time{}, false } - age := now.Sub(mainThreadClockBase) - lastElapsed - if age < 0 { - age = 0 - } + age := max(now.Sub(mainThreadClockBase)-lastElapsed, 0) return age, time.Unix(0, lastWall), true } diff --git a/desktop/hang_watchdog_windows.go b/desktop/hang_watchdog_windows.go index 209060bed1..226a5021d8 100644 --- a/desktop/hang_watchdog_windows.go +++ b/desktop/hang_watchdog_windows.go @@ -113,10 +113,7 @@ func windowClassName(hwnd uintptr) string { } func windowMessageLoopResponsive(hwnd uintptr, timeout time.Duration) bool { - timeoutMS := timeout.Milliseconds() - if timeoutMS < 250 { - timeoutMS = 250 - } + timeoutMS := max(timeout.Milliseconds(), 250) var result uintptr ok, _, _ := sendMessageTimeoutProc.Call( hwnd, diff --git a/desktop/hang_watchdog_windows_test.go b/desktop/hang_watchdog_windows_test.go index cca9a81d11..430fc06c54 100644 --- a/desktop/hang_watchdog_windows_test.go +++ b/desktop/hang_watchdog_windows_test.go @@ -12,7 +12,7 @@ func TestCurrentProcessTopLevelWindowReusesEnumCallback(t *testing.T) { // Go's Windows runtime currently supports a finite callback table. The old // implementation allocated one callback per poll and exhausted that table // during a normal long-running session. - for i := 0; i < 2100; i++ { + for range 2100 { _ = currentProcessTopLevelWindow() } } diff --git a/desktop/heartbeat.go b/desktop/heartbeat.go index 4596bce7da..317286473a 100644 --- a/desktop/heartbeat.go +++ b/desktop/heartbeat.go @@ -319,7 +319,7 @@ func (e *HeartbeatEngine) executeTask(t HeartbeatTask) HeartbeatTask { // Wait for the tab's controller to be built (it's started // asynchronously in a goroutine by openTopicTab). var ctrl heartbeatRuntimeStatus - for i := 0; i < 40; i++ { + for range 40 { if candidate := e.app.ctrlByTabID(tabMeta.ID); candidate != nil { ctrl = candidate break @@ -597,11 +597,11 @@ type heartbeatSchedule struct { } func parseHeartbeatSchedule(interval string) (heartbeatSchedule, bool) { - idx := strings.Index(interval, "|") - if idx < 0 { + _, after, ok0 := strings.Cut(interval, "|") + if !ok0 { return heartbeatSchedule{}, false } - raw := strings.TrimSpace(interval[idx+1:]) + raw := strings.TrimSpace(after) if raw == "" { return heartbeatSchedule{}, false } @@ -625,7 +625,7 @@ func parseHeartbeatSchedule(interval string) (heartbeatSchedule, bool) { case "daily": return s, true case "weekly", "biweekly": - for _, part := range strings.Split(rule, ",") { + for part := range strings.SplitSeq(rule, ",") { if wd, ok := parseHeartbeatWeekday(part); ok { s.days = append(s.days, wd) } @@ -681,7 +681,7 @@ func previousHeartbeatScheduleAt(t HeartbeatTask, now time.Time) (time.Time, boo func previousHeartbeatWeeklyAt(s heartbeatSchedule, now time.Time, windowDays int, anchor time.Time) (time.Time, bool) { var best time.Time - for offset := 0; offset < windowDays; offset++ { + for offset := range windowDays { day := now.AddDate(0, 0, -offset) for _, wd := range s.days { if day.Weekday() != wd { diff --git a/desktop/heartbeat_test.go b/desktop/heartbeat_test.go index d187dc4aa1..b69b8729f0 100644 --- a/desktop/heartbeat_test.go +++ b/desktop/heartbeat_test.go @@ -155,7 +155,7 @@ func TestHeartbeatExecuteTaskPersistsFreshConversationTopicID(t *testing.T) { app := NewApp() app.ctx = context.Background() app.readyHook = func() {} - app.runtimeEvents.emit = func(context.Context, string, ...interface{}) {} + app.runtimeEvents.emit = func(context.Context, string, ...any) {} engine := &HeartbeatEngine{ app: app, pendingTopics: map[string]heartbeatPendingTopic{}, @@ -236,7 +236,7 @@ func TestHeartbeatExecuteTaskSkipsPendingPrompt(t *testing.T) { app := NewApp() app.ctx = context.Background() app.readyHook = func() {} - app.runtimeEvents.emit = func(context.Context, string, ...interface{}) {} + app.runtimeEvents.emit = func(context.Context, string, ...any) {} engine := &HeartbeatEngine{ app: app, pendingTopics: map[string]heartbeatPendingTopic{}, diff --git a/desktop/history_perf_test.go b/desktop/history_perf_test.go index f7d2eef51b..f1cf8ed795 100644 --- a/desktop/history_perf_test.go +++ b/desktop/history_perf_test.go @@ -23,7 +23,7 @@ func syntheticHistoryMessages(turns, outputSize int, toolName string, failed boo if failed { output = "error: " + output } - for i := 0; i < turns; i++ { + for i := range turns { callID := fmt.Sprintf("call_%d", i) msgs = append(msgs, provider.Message{Role: provider.RoleUser, Content: fmt.Sprintf("prompt %d", i)}, @@ -66,7 +66,7 @@ func BenchmarkHistoryMessagesSynthetic(b *testing.B) { b.ReportAllocs() b.SetBytes(inputBytes) b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { got := historyMessages(msgs, func(content string) string { return content }) if len(got) == 0 { b.Fatal("empty history") @@ -84,7 +84,7 @@ func BenchmarkHistoryMessagesMarshalSynthetic(b *testing.B) { b.ReportAllocs() b.SetBytes(inputBytes) b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { got := historyMessages(msgs, func(content string) string { return content }) encoded, err := json.Marshal(got) if err != nil { diff --git a/desktop/hooks_settings_app.go b/desktop/hooks_settings_app.go index bc0e96710d..b25a50a3e5 100644 --- a/desktop/hooks_settings_app.go +++ b/desktop/hooks_settings_app.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "strings" fileencoding "reasonix/internal/fileutil/encoding" @@ -127,12 +128,7 @@ func hookEventNames() []string { } func validHookEvent(event hook.Event) bool { - for _, e := range hook.Events { - if event == e { - return true - } - } - return false + return slices.Contains(hook.Events, event) } func hookConfigView(event hook.Event, cfg hook.HookConfig) HookConfigView { diff --git a/desktop/main_test.go b/desktop/main_test.go index 06bab18acc..e7bc984fcb 100644 --- a/desktop/main_test.go +++ b/desktop/main_test.go @@ -64,7 +64,7 @@ func TestMain(m *testing.M) { // contexts tests use, killing the process from any emitting code path. // Tests that assert on runtime events install their own capture through // the per-instance runtimeEvents.emit hook, which takes precedence. - runtimeEventsEmitFallback = func(context.Context, string, ...interface{}) {} + runtimeEventsEmitFallback = func(context.Context, string, ...any) {} code := m.Run() os.RemoveAll(dir) os.Exit(code) diff --git a/desktop/metrics_app.go b/desktop/metrics_app.go index 9de5ba8ce3..ac7dcc9cd0 100644 --- a/desktop/metrics_app.go +++ b/desktop/metrics_app.go @@ -9,6 +9,7 @@ import ( "path/filepath" "regexp" "runtime" + "slices" "strconv" "strings" "sync" @@ -102,10 +103,8 @@ func countBucket(n int) string { func knownBucket(value string, allowed ...string) string { value = strings.ToLower(strings.TrimSpace(value)) - for _, ok := range allowed { - if value == ok { - return value - } + if slices.Contains(allowed, value) { + return value } return "other" } @@ -433,7 +432,7 @@ func (m *metricsAggregator) observeRecoveryMetrics(stats recovery.Metrics) { return } add := func(signal string, n int64) { - for i := int64(0); i < n; i++ { + for range n { m.inc(signal, "total") } } diff --git a/desktop/race_regression_test.go b/desktop/race_regression_test.go index 47320554e0..a401ec128e 100644 --- a/desktop/race_regression_test.go +++ b/desktop/race_regression_test.go @@ -36,14 +36,14 @@ func TestSessionLeaseHelpersConcurrentAccess(t *testing.T) { wg.Add(3) go func() { defer wg.Done() - for i := 0; i < iterations; i++ { + for range iterations { _ = tabA.ensureSessionLease(path) _ = tabA.sessionLeaseRuntimeKey() } }() go func() { defer wg.Done() - for i := 0; i < iterations; i++ { + for range iterations { // The applyRuntimeTab transfer shape: move A's lease to B and back. tabB.adoptSessionLease(tabA.takeSessionLease()) tabA.adoptSessionLease(tabB.takeSessionLease()) @@ -51,7 +51,7 @@ func TestSessionLeaseHelpersConcurrentAccess(t *testing.T) { }() go func() { defer wg.Done() - for i := 0; i < iterations; i++ { + for range iterations { tabA.releaseSessionLease() } }() @@ -142,13 +142,13 @@ func TestTabEventSinkEmitConcurrentRebind(t *testing.T) { wg.Add(2) go func() { defer wg.Done() - for i := 0; i < iterations; i++ { + for range iterations { sink.Emit(event.Event{Kind: event.Notice, Text: "hammer"}) } }() go func() { defer wg.Done() - for i := 0; i < iterations; i++ { + for i := range iterations { sink.setBinding(fmt.Sprintf("tab-%d", i%2), nil) sink.clearContext() } @@ -170,7 +170,7 @@ func TestModeRebuildAndABANavigationFenceLeaseFailureAndOldEpochEvent(t *testing continueCommit := make(chan struct{}) oldEvent := make(chan wireEventTab, 1) var capturedOldEvent wireEventTab - oldSink.runtimeEvents.emit = func(_ context.Context, name string, payload ...interface{}) { + oldSink.runtimeEvents.emit = func(_ context.Context, name string, payload ...any) { if name != eventChannel || len(payload) != 1 { return } @@ -573,7 +573,7 @@ func TestMetaForTabConcurrentWithBuildSwap(t *testing.T) { done := make(chan struct{}) go func() { defer close(done) - for i := 0; i < iterations; i++ { + for i := range iterations { app.mu.Lock() tab.Ready = !tab.Ready tab.Label = fmt.Sprintf("model-%d", i) @@ -584,7 +584,7 @@ func TestMetaForTabConcurrentWithBuildSwap(t *testing.T) { app.mu.Unlock() } }() - for i := 0; i < iterations; i++ { + for range iterations { meta := app.MetaForTab("tab") if meta.EventChannel == "" { t.Fatal("MetaForTab returned zero meta for a live tab") diff --git a/desktop/recovery_gc_test.go b/desktop/recovery_gc_test.go index 321f567a36..97da77e20a 100644 --- a/desktop/recovery_gc_test.go +++ b/desktop/recovery_gc_test.go @@ -1,7 +1,6 @@ package main import ( - "context" "os" "path/filepath" "testing" @@ -106,8 +105,7 @@ func TestRecoveryGCFirstSweepWaitsForTabRestore(t *testing.T) { } _, branchPath := forkCoveredRecoveryBranch(t, dir, "startup") - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := t.Context() app := &App{ ctx: ctx, tabs: map[string]*WorkspaceTab{}, diff --git a/desktop/reload_runtime_test.go b/desktop/reload_runtime_test.go index a10b29483c..7105a17ce1 100644 --- a/desktop/reload_runtime_test.go +++ b/desktop/reload_runtime_test.go @@ -5,6 +5,7 @@ import ( "io" "os" "path/filepath" + "slices" "sync" "testing" "time" @@ -77,7 +78,7 @@ func TestReloadRuntimeSwapsAndClosesOldAfterSwap(t *testing.T) { app.readyHook = func() {} var fenceMu sync.Mutex var fenceNames []string - app.runtimeEvents.emit = func(_ context.Context, name string, _ ...interface{}) { + app.runtimeEvents.emit = func(_ context.Context, name string, _ ...any) { fenceMu.Lock() fenceNames = append(fenceNames, name) fenceMu.Unlock() @@ -131,13 +132,7 @@ func TestReloadRuntimeSwapsAndClosesOldAfterSwap(t *testing.T) { deadline := time.Now().Add(2 * time.Second) for { fenceMu.Lock() - found := false - for _, name := range fenceNames { - if name == "runtime:rebuilt" { - found = true - break - } - } + found := slices.Contains(fenceNames, "runtime:rebuilt") fenceMu.Unlock() if found { break diff --git a/desktop/remote_app_test.go b/desktop/remote_app_test.go index ba045ec5cd..00b5767497 100644 --- a/desktop/remote_app_test.go +++ b/desktop/remote_app_test.go @@ -165,7 +165,7 @@ func TestConfirmRemoteSecretDelegatesWithoutPersisting(t *testing.T) { func TestRemoteStatusBridgesToAsyncEmitter(t *testing.T) { a := &App{ctx: context.Background()} events := make(chan runtimeEventEnvelope, 4) - a.runtimeEvents.emit = func(ctx context.Context, name string, payload ...interface{}) { + a.runtimeEvents.emit = func(ctx context.Context, name string, payload ...any) { events <- runtimeEventEnvelope{ctx: ctx, name: name, payload: payload} } a.onStatus(RemoteConnectionStatusView{HostID: "box", State: "connected"}) diff --git a/desktop/remote_hosts.go b/desktop/remote_hosts.go index d9f7b99ae2..bf0c3704e6 100644 --- a/desktop/remote_hosts.go +++ b/desktop/remote_hosts.go @@ -194,7 +194,7 @@ func canonicalRemoteSSHHost(host string) (string, error) { if trimmed == "" || len(trimmed) > 253 { return "", errors.New("remote SSH host is invalid") } - for _, label := range strings.Split(trimmed, ".") { + for label := range strings.SplitSeq(trimmed, ".") { if !remoteDNSLabelPattern.MatchString(label) { return "", errors.New("remote SSH host is invalid") } diff --git a/desktop/remote_hosts_test.go b/desktop/remote_hosts_test.go index 553de986b6..ec902c4cf2 100644 --- a/desktop/remote_hosts_test.go +++ b/desktop/remote_hosts_test.go @@ -100,10 +100,10 @@ func TestRemoteHostStoreLoadsV1AsConfigAndWritesV2OnMutation(t *testing.T) { if err != nil { t.Fatal(err) } - raw := []byte(fmt.Sprintf( + raw := fmt.Appendf(nil, `{"version":1,"hosts":[{"id":%q,"alias":%q,"label":%q,"sshConfigPath":%q,"clientInstanceId":%q,"resumeLeaseId":"lease_legacy","layoutRef":"layout_legacy"}]}`, entry.ID, entry.Alias, entry.Label, configPath, entry.ClientInstanceID, - )) + ) if err := os.WriteFile(path, raw, 0o600); err != nil { t.Fatal(err) } @@ -213,7 +213,7 @@ func TestRemoteHostStoreConcurrentInstancesDoNotLoseUpdates(t *testing.T) { const count = 48 var wg sync.WaitGroup errorsCh := make(chan error, count) - for i := 0; i < count; i++ { + for i := range count { wg.Add(1) go func(i int) { defer wg.Done() diff --git a/desktop/remote_lifecycle_test.go b/desktop/remote_lifecycle_test.go index 7124ff2fe7..97bdf67bf0 100644 --- a/desktop/remote_lifecycle_test.go +++ b/desktop/remote_lifecycle_test.go @@ -373,15 +373,13 @@ func TestHostKeyPromptsAreSerializedForGlobalDialog(t *testing.T) { prompts = append(prompts, pendingPrompt{hostID: hostID, prompt: mgr.hostKeyPrompt(hostID, mh)}) } for _, pending := range prompts { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { _, _ = pending.prompt(ctx, remote.HostKeyQuestion{ Address: pending.hostID + ":22", KeyType: "ssh-ed25519", Fingerprint: pending.hostID, }) - }() + }) } first := <-sink.statuses diff --git a/desktop/remote_window.go b/desktop/remote_window.go index ab1580c8de..ab82342861 100644 --- a/desktop/remote_window.go +++ b/desktop/remote_window.go @@ -574,8 +574,8 @@ func (a *App) domReadyRemoteWindow() { func (a *App) secondInstanceRemoteWindow(data options.SecondInstanceData) { ticket := "" for _, arg := range data.Args { - if strings.HasPrefix(arg, remoteWindowTicketArgPrefix) { - ticket = strings.TrimPrefix(arg, remoteWindowTicketArgPrefix) + if after, ok := strings.CutPrefix(arg, remoteWindowTicketArgPrefix); ok { + ticket = after break } } diff --git a/desktop/remote_window_test.go b/desktop/remote_window_test.go index 8e97992bd1..fde86c0b90 100644 --- a/desktop/remote_window_test.go +++ b/desktop/remote_window_test.go @@ -289,8 +289,7 @@ func TestRemoteWindowOwnerWaitDetectsParentExit(t *testing.T) { if err := cmd.Start(); err != nil { t.Fatal(err) } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := t.Context() exited := make(chan bool, 1) go func() { exited <- waitForRemoteWindowOwnerExit(ctx, cmd.Process.Pid) }() if err := cmd.Process.Kill(); err != nil { diff --git a/desktop/runtime_rebuilt_event_test.go b/desktop/runtime_rebuilt_event_test.go index 4a4c4f088f..35f7562194 100644 --- a/desktop/runtime_rebuilt_event_test.go +++ b/desktop/runtime_rebuilt_event_test.go @@ -59,14 +59,14 @@ func TestRuntimeRebuildsEmitRuntimeRebuiltForTab(t *testing.T) { // The App-level queue must stay silent: ordering against the tab's agent // events only holds when the notice rides the tab sink's own queue, so a // notice showing up here means the routing regressed to the fallback. - app.runtimeEvents.emit = func(_ context.Context, name string, _ ...interface{}) { + app.runtimeEvents.emit = func(_ context.Context, name string, _ ...any) { if name == "runtime:rebuilt" { mu.Lock() rebuilt = append(rebuilt, "VIA-APP-QUEUE") mu.Unlock() } } - sinkEmit := func(_ context.Context, name string, payload ...interface{}) { + sinkEmit := func(_ context.Context, name string, payload ...any) { if name != "runtime:rebuilt" { return } diff --git a/desktop/session_prompt_test.go b/desktop/session_prompt_test.go index 9cd76f1acf..4cf988bfb4 100644 --- a/desktop/session_prompt_test.go +++ b/desktop/session_prompt_test.go @@ -172,7 +172,7 @@ func TestParallelDesktopTabsPersistCompleteTranscriptsAcrossReload(t *testing.T) tabOrder := make([]string, 0, tabCount) controllers := make([]*control.Controller, 0, tabCount) - for tabIndex := 0; tabIndex < tabCount; tabIndex++ { + for tabIndex := range tabCount { id := fmt.Sprintf("parallel-%02d", tabIndex) path := filepath.Join(dir, id+".jsonl") prov := &capturingProvider{} @@ -207,25 +207,20 @@ func TestParallelDesktopTabsPersistCompleteTranscriptsAcrossReload(t *testing.T) errs := make(chan error, tabCount+1) var wg sync.WaitGroup for tabIndex, ctrl := range controllers { - tabIndex, ctrl := tabIndex, ctrl - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { <-start - for turn := 0; turn < turnsPerTab; turn++ { + for turn := range turnsPerTab { input := fmt.Sprintf("tab-%02d-turn-%02d", tabIndex, turn) if err := ctrl.RunTurn(context.Background(), input); err != nil { errs <- fmt.Errorf("%s: %w", input, err) return } } - }() + }) } - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { <-start - for round := 0; round < 3; round++ { + for range 3 { for _, id := range tabOrder { if err := app.SetActiveTab(id); err != nil { errs <- fmt.Errorf("switch to %s: %w", id, err) @@ -233,7 +228,7 @@ func TestParallelDesktopTabsPersistCompleteTranscriptsAcrossReload(t *testing.T) } } } - }() + }) close(start) wg.Wait() @@ -269,7 +264,7 @@ func TestParallelDesktopTabsPersistCompleteTranscriptsAcrossReload(t *testing.T) if msgs[0].Role != provider.RoleSystem || msgs[0].Content != systemText { t.Fatalf("%s system message = %+v", id, msgs[0]) } - for turn := 0; turn < turnsPerTab; turn++ { + for turn := range turnsPerTab { user := msgs[1+turn*2] assistant := msgs[2+turn*2] wantUser := fmt.Sprintf("tab-%02d-turn-%02d", tabIndex, turn) diff --git a/desktop/session_recovery_cleanup_test.go b/desktop/session_recovery_cleanup_test.go index eb3dee625a..037aebeaf1 100644 --- a/desktop/session_recovery_cleanup_test.go +++ b/desktop/session_recovery_cleanup_test.go @@ -17,7 +17,7 @@ import ( func saveSnapshotTurns(t *testing.T, path string, turns int) *agent.Session { t.Helper() s := agent.NewSession("sys") - for i := 0; i < turns; i++ { + for i := range turns { s.Add(provider.Message{Role: provider.RoleUser, Content: "prompt " + string(rune('a'+i))}) s.Add(provider.Message{Role: provider.RoleAssistant, Content: "reply"}) if err := s.SaveSnapshot(path); err != nil { diff --git a/desktop/sessions.go b/desktop/sessions.go index bcf5c40e83..8d38d720c9 100644 --- a/desktop/sessions.go +++ b/desktop/sessions.go @@ -354,7 +354,7 @@ func reserveUniqueSessionTrashItemDir(dir, key string) (string, error) { return "", err } stem := strings.TrimSuffix(key, ".jsonl") - for i := 0; i < 100; i++ { + for i := range 100 { name := fmt.Sprintf("%s.jsonl-deleted-%d-%02d", stem, time.Now().UnixNano(), i) itemDir := filepath.Join(root, name) if err := os.Mkdir(itemDir, 0o755); err == nil { diff --git a/desktop/sessions_test.go b/desktop/sessions_test.go index ab744e7abc..d61167b312 100644 --- a/desktop/sessions_test.go +++ b/desktop/sessions_test.go @@ -30,7 +30,7 @@ func occupyReadFileWithTimeoutSlots(t *testing.T) func() { return } released = true - for i := 0; i < filled; i++ { + for range filled { <-readFileWithTimeoutSlots } } @@ -1208,7 +1208,7 @@ func TestRecordSessionPlannerDisplayConcurrentPreservesEverySession(t *testing.T start := make(chan struct{}) errs := make(chan error, writers) var wg sync.WaitGroup - for i := 0; i < writers; i++ { + for i := range writers { wg.Add(1) go func(i int) { defer wg.Done() @@ -1232,7 +1232,7 @@ func TestRecordSessionPlannerDisplayConcurrentPreservesEverySession(t *testing.T if len(got) != writers { t.Fatalf("planner display sessions = %d, want %d", len(got), writers) } - for i := 0; i < writers; i++ { + for i := range writers { key := fmt.Sprintf("session-%02d.jsonl", i) if len(got[key]) != 1 || len(got[key][0].Messages) != 1 || got[key][0].Messages[0].Content != fmt.Sprintf("answer-%02d", i) { t.Fatalf("planner display %s = %+v", key, got[key]) @@ -1504,7 +1504,7 @@ func TestRecordSessionDisplaySerializesConcurrentTabs(t *testing.T) { const tabs = 32 errs := make(chan error, tabs) var wg sync.WaitGroup - for i := 0; i < tabs; i++ { + for i := range tabs { wg.Add(1) go func(i int) { defer wg.Done() @@ -1523,7 +1523,7 @@ func TestRecordSessionDisplaySerializesConcurrentTabs(t *testing.T) { } got := loadSessionDisplays(dir) - for i := 0; i < tabs; i++ { + for i := range tabs { key := fmt.Sprintf("tab-%02d.jsonl", i) content := fmt.Sprintf("expanded-%02d", i) if display := got[key][messageDisplayKey(content)]; display != fmt.Sprintf("display-%02d", i) { diff --git a/desktop/settings_app.go b/desktop/settings_app.go index 155bd83e55..e86249451b 100644 --- a/desktop/settings_app.go +++ b/desktop/settings_app.go @@ -13,6 +13,7 @@ import ( "os" "path/filepath" "runtime" + "slices" "sort" "strings" "sync" @@ -1687,13 +1688,13 @@ func configDeclaresProviderAccess(path string) bool { if err != nil { return false } - for _, line := range strings.Split(string(body), "\n") { + for line := range strings.SplitSeq(string(body), "\n") { if before, _, ok := strings.Cut(line, "#"); ok { line = before } line = strings.TrimSpace(line) - if strings.HasPrefix(line, "provider_access") { - rest := strings.TrimSpace(strings.TrimPrefix(line, "provider_access")) + if after, ok := strings.CutPrefix(line, "provider_access"); ok { + rest := strings.TrimSpace(after) return strings.HasPrefix(rest, "=") } } @@ -2321,10 +2322,8 @@ func providerVisionModels(models, visionModels []string) []string { func providerDefaultForModels(currentDefault string, models []string) string { currentDefault = strings.TrimSpace(currentDefault) if currentDefault != "" { - for _, model := range models { - if model == currentDefault { - return currentDefault - } + if slices.Contains(models, currentDefault) { + return currentDefault } } if len(models) > 0 { diff --git a/desktop/shared_host.go b/desktop/shared_host.go index 3126a707a3..1c18144d3e 100644 --- a/desktop/shared_host.go +++ b/desktop/shared_host.go @@ -73,7 +73,7 @@ func (a *App) reapOrphanCodeGraph() { ours := map[int]bool{} out, err := exec.Command("pgrep", "-P", strconv.Itoa(myPID)).Output() if err == nil { - for _, f := range strings.Fields(string(out)) { + for f := range strings.FieldsSeq(string(out)) { if pid, err := strconv.Atoi(f); err == nil { ours[pid] = true } @@ -85,7 +85,7 @@ func (a *App) reapOrphanCodeGraph() { if err != nil { return } - for _, f := range strings.Fields(string(out)) { + for f := range strings.FieldsSeq(string(out)) { pid, err := strconv.Atoi(f) if err != nil || pid == myPID || ours[pid] { continue diff --git a/desktop/tab_event_sink_test.go b/desktop/tab_event_sink_test.go index df543dbca8..f24ec5e02a 100644 --- a/desktop/tab_event_sink_test.go +++ b/desktop/tab_event_sink_test.go @@ -110,7 +110,7 @@ func TestTabEventSinkDoesNotBlockOnRuntimeEventsEmit(t *testing.T) { var calls atomic.Int32 sink := &tabEventSink{tabID: "tab", ctx: context.Background()} - sink.runtimeEvents.emit = func(_ context.Context, name string, payload ...interface{}) { + sink.runtimeEvents.emit = func(_ context.Context, name string, payload ...any) { if name != eventChannel { t.Errorf("event name = %q, want %q", name, eventChannel) } @@ -170,7 +170,7 @@ func TestEmitProjectTreeChangedDoesNotBlockOnRuntimeEventsEmit(t *testing.T) { var calls atomic.Int32 app := &App{ctx: context.Background()} - app.runtimeEvents.emit = func(_ context.Context, name string, payload ...interface{}) { + app.runtimeEvents.emit = func(_ context.Context, name string, payload ...any) { if name != "project-tree:changed" { t.Errorf("event name = %q, want project-tree:changed", name) } @@ -221,7 +221,7 @@ func TestAsyncRuntimeEmitterDrainsBacklogInOrder(t *testing.T) { var calls atomic.Int32 emitter := &asyncRuntimeEmitter{} - emitter.emit = func(_ context.Context, _ string, payload ...interface{}) { + emitter.emit = func(_ context.Context, _ string, payload ...any) { if len(payload) != 1 { t.Errorf("payload count = %d, want 1", len(payload)) return @@ -239,7 +239,7 @@ func TestAsyncRuntimeEmitterDrainsBacklogInOrder(t *testing.T) { } ctx := context.Background() - for i := 0; i < backlog; i++ { + for i := range backlog { emitter.Emit(ctx, "agent:event", strconv.Itoa(i)) } @@ -250,7 +250,7 @@ func TestAsyncRuntimeEmitterDrainsBacklogInOrder(t *testing.T) { } close(release) - for i := 0; i < backlog; i++ { + for i := range backlog { select { case got := <-delivered: if want := strconv.Itoa(i); got != want { diff --git a/desktop/tabs.go b/desktop/tabs.go index 42d7b6e2f6..7271780db7 100644 --- a/desktop/tabs.go +++ b/desktop/tabs.go @@ -9,8 +9,10 @@ import ( "errors" "fmt" "log/slog" + "maps" "os" "path/filepath" + "slices" "sort" "strings" "sync" @@ -308,15 +310,11 @@ func cloneSessionUsageStats(in sessionUsageStats) sessionUsageStats { out := in if len(in.Sources) > 0 { out.Sources = make(map[string]usageSourceStats, len(in.Sources)) - for source, stats := range in.Sources { - out.Sources[source] = stats - } + maps.Copy(out.Sources, in.Sources) } if len(in.sourceSessionCache) > 0 { out.sourceSessionCache = make(map[string]sourceSessionCacheCounters, len(in.sourceSessionCache)) - for source, counters := range in.sourceSessionCache { - out.sourceSessionCache[source] = counters - } + maps.Copy(out.sourceSessionCache, in.sourceSessionCache) } return out } @@ -1197,9 +1195,7 @@ func (t *WorkspaceTab) telemetrySnapshot() tabTelemetrySnapshot { } if len(t.usageTelemetry.Sources) > 0 { usage.Sources = make(map[string]usageSourceStats, len(t.usageTelemetry.Sources)) - for source, stats := range t.usageTelemetry.Sources { - usage.Sources[source] = stats - } + maps.Copy(usage.Sources, t.usageTelemetry.Sources) } usage.activeTurnStartedAt = 0 usage.sourceSessionCache = nil @@ -1408,9 +1404,9 @@ func updateBufferedHistoryToolCallSummary(messages []*bufferedHistoryMessage, ca if callID == "" { return } - for i := len(messages) - 1; i >= 0; i-- { - for j := range messages[i].message.ToolCalls { - call := &messages[i].message.ToolCalls[j] + for _, v := range slices.Backward(messages) { + for j := range v.message.ToolCalls { + call := &v.message.ToolCalls[j] if call.ID != callID { continue } @@ -1737,7 +1733,7 @@ func (s *tabEventSink) context() context.Context { return s.ctx } -func (s *tabEventSink) emitRuntimeEvent(name string, payload ...interface{}) { +func (s *tabEventSink) emitRuntimeEvent(name string, payload ...any) { if s == nil { return } @@ -1748,12 +1744,12 @@ func (s *tabEventSink) emitRuntimeEvent(name string, payload ...interface{}) { s.runtimeEvents.Emit(ctx, name, payload...) } -type runtimeEventEmitFunc func(context.Context, string, ...interface{}) +type runtimeEventEmitFunc func(context.Context, string, ...any) type runtimeEventEnvelope struct { ctx context.Context name string - payload []interface{} + payload []any } // asyncRuntimeEmitter decouples Wails' runtime event bridge from agent @@ -1775,14 +1771,14 @@ type asyncRuntimeEmitter struct { running bool } -func (e *asyncRuntimeEmitter) Emit(ctx context.Context, name string, payload ...interface{}) { +func (e *asyncRuntimeEmitter) Emit(ctx context.Context, name string, payload ...any) { if ctx == nil { return } item := runtimeEventEnvelope{ ctx: ctx, name: name, - payload: append([]interface{}(nil), payload...), + payload: append([]any(nil), payload...), } e.mu.Lock() e.queue = append(e.queue, item) @@ -2077,8 +2073,8 @@ func lastHistoryMessageIsUser(history []provider.Message) bool { } func hasPendingInterruptedRecovery(history []provider.Message) bool { - for i := len(history) - 1; i >= 0; i-- { - m := history[i] + for _, v := range slices.Backward(history) { + m := v if m.LocalOnly && m.InterruptedTurn != nil { return m.InterruptedTurn.Pending } @@ -2104,9 +2100,9 @@ func (s *tabEventSink) eventTabAndController() (*WorkspaceTab, control.SessionAP } func lastUserMessageContent(msgs []provider.Message) string { - for i := len(msgs) - 1; i >= 0; i-- { - if msgs[i].Role == provider.RoleUser { - return agent.UserMessageText(msgs[i]) + for _, v := range slices.Backward(msgs) { + if v.Role == provider.RoleUser { + return agent.UserMessageText(v) } } return "" @@ -2892,7 +2888,7 @@ func createEmptySessionFile(dir, model string) (string, error) { if err := os.MkdirAll(dir, 0o755); err != nil { return "", err } - for i := 0; i < 3; i++ { + for range 3 { path := agent.NewSessionPath(dir, model) f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o644) if err == nil { @@ -3232,10 +3228,7 @@ func (a *App) closeTab(tabID string, allowDetach bool) error { if wasActive { a.activeTabID = "" if len(a.tabOrder) > 0 { - nextIndex := closedIndex - if nextIndex < 0 { - nextIndex = 0 - } + nextIndex := max(closedIndex, 0) if nextIndex >= len(a.tabOrder) { nextIndex = len(a.tabOrder) - 1 } @@ -4673,7 +4666,7 @@ func autoTopicTitleProposalFromSession(path string) autoTopicTitleProposal { if title == "" { return autoTopicTitleProposal{} } - sum := sha256.Sum256([]byte(fmt.Sprintf("%d\x00%s", stage, strings.Join(basis, "\x00")))) + sum := sha256.Sum256(fmt.Appendf(nil, "%d\x00%s", stage, strings.Join(basis, "\x00"))) return autoTopicTitleProposal{ Title: title, Stage: stage, @@ -4799,10 +4792,7 @@ func topicTitleFromUserTurns(users []string) string { continue } runes := len([]rune(title)) - score := runes - if score > 24 { - score = 24 - } + score := min(runes, 24) if i == 0 { score += 3 } @@ -5443,12 +5433,7 @@ func containsDesktopString(values []string, value string) bool { if value == "" { return false } - for _, item := range uniqueStrings(values) { - if item == value { - return true - } - } - return false + return slices.Contains(uniqueStrings(values), value) } func pinnedTopicIDs(topicIDs []string, pinned []string) []string { @@ -7484,7 +7469,7 @@ func (a *App) emitProjectTreeChangedEvent() { a.emitRuntimeEvent("project-tree:changed") } -func (a *App) emitRuntimeEvent(name string, payload ...interface{}) { +func (a *App) emitRuntimeEvent(name string, payload ...any) { if a == nil || a.ctx == nil { return } @@ -7875,7 +7860,7 @@ func (a *App) ListProjectTree() []ProjectNode { continue } pendingLoads++ - dir := dir // capture + // capture cacheToken := projectSessionCache.versionToken(dir) go func() { result := sessionDirLoadResult{dir: dir} @@ -8078,17 +8063,14 @@ func (a *App) ListProjectTree() []ProjectNode { projectTopicResults := make([]projectTopics, len(f.Projects)) var topicLoadWg sync.WaitGroup for i, p := range f.Projects { - i, p := i, p - topicLoadWg.Add(1) - go func() { - defer topicLoadWg.Done() + topicLoadWg.Go(func() { projectTopicResults[i] = projectTopics{ project: p, titles: loadTopicTitles(p.Root), sources: loadTopicTitleSources(p.Root), createdAts: loadTopicCreatedAts(p.Root), } - }() + }) } topicLoadWg.Wait() for _, loaded := range projectTopicResults { diff --git a/desktop/tabs_display_buffer_test.go b/desktop/tabs_display_buffer_test.go index 96493109e3..04db67cedc 100644 --- a/desktop/tabs_display_buffer_test.go +++ b/desktop/tabs_display_buffer_test.go @@ -57,9 +57,9 @@ func TestDisplayTurnBufferStreamingAllocationsStayNearLinear(t *testing.T) { chunk := strings.Repeat("x", chunkSize) result := testing.Benchmark(func(b *testing.B) { b.ReportAllocs() - for i := 0; i < b.N; i++ { + for range b.N { var buffer displayTurnBuffer - for part := 0; part < chunks; part++ { + for range chunks { recordHistoryDisplayEvent(&buffer, event.Event{Kind: event.Text, Text: chunk}) } messages := buffer.materialize() diff --git a/desktop/tabs_order_test.go b/desktop/tabs_order_test.go index 7b982301f5..1d1f3c350b 100644 --- a/desktop/tabs_order_test.go +++ b/desktop/tabs_order_test.go @@ -33,7 +33,7 @@ func testAppWithOrderedTabs(t *testing.T, active string, ids ...string) *App { } func installNoopRuntimeEvents(app *App, sinks ...*tabEventSink) { - emit := func(context.Context, string, ...interface{}) {} + emit := func(context.Context, string, ...any) {} if app != nil { app.runtimeEvents.emit = emit } @@ -664,18 +664,16 @@ func TestListTabsRepairsStaleOrderWithoutRacing(t *testing.T) { if testing.Short() { iterations = 5 } - for i := 0; i < 8; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range 8 { + wg.Go(func() { <-start - for j := 0; j < iterations; j++ { + for range iterations { if got := strings.Join(tabIDs(app.ListTabs()), ","); got != "a,b,c" { errs <- got return } } - }() + }) } close(start) wg.Wait() diff --git a/desktop/tabs_runtime_status_test.go b/desktop/tabs_runtime_status_test.go index b3bcaed6ab..5527856f5a 100644 --- a/desktop/tabs_runtime_status_test.go +++ b/desktop/tabs_runtime_status_test.go @@ -224,10 +224,10 @@ func TestProjectTreeShowsBackgroundJobStatus(t *testing.T) { func TestBackgroundJobNoticeForcesProjectTreeRefresh(t *testing.T) { isolateDesktopUserDirs(t) - var refreshes int32 + var refreshes atomic.Int32 app := NewApp() app.projectTreeChangedHook = func() { - atomic.AddInt32(&refreshes, 1) + refreshes.Add(1) } app.tabs["job"] = &WorkspaceTab{ ID: "job", @@ -239,7 +239,7 @@ func TestBackgroundJobNoticeForcesProjectTreeRefresh(t *testing.T) { sink := &tabEventSink{tabID: "job", app: app} sink.Emit(event.Event{Kind: event.Notice, Level: event.LevelInfo, Text: "background bash finished: bash-1"}) - if got := atomic.LoadInt32(&refreshes); got != 1 { + if got := refreshes.Load(); got != 1 { t.Fatalf("project-tree refreshes = %d, want 1 for background job finish notice", got) } } diff --git a/desktop/tabs_sink_context_test.go b/desktop/tabs_sink_context_test.go index a5a68013bc..affd2bb765 100644 --- a/desktop/tabs_sink_context_test.go +++ b/desktop/tabs_sink_context_test.go @@ -19,7 +19,7 @@ func TestTabEventSinkClearContextStopsEmission(t *testing.T) { var mu sync.Mutex var emitted int s := &tabEventSink{tabID: "t"} - s.runtimeEvents.emit = func(context.Context, string, ...interface{}) { + s.runtimeEvents.emit = func(context.Context, string, ...any) { mu.Lock() emitted++ mu.Unlock() diff --git a/desktop/tabs_topic_test.go b/desktop/tabs_topic_test.go index ef257a0479..90233538a2 100644 --- a/desktop/tabs_topic_test.go +++ b/desktop/tabs_topic_test.go @@ -3971,18 +3971,16 @@ func TestLegacyMigrationConcurrentRunsHaveNoLostUpdates(t *testing.T) { } const n = 8 want := make(map[string]bool, n) - for i := 0; i < n; i++ { + for i := range n { p := writeLegacySession(t, dir, fmt.Sprintf("legacy-%d.jsonl", i), "hi", time.Now()) want[legacySessionTopicID(p)] = true } var wg sync.WaitGroup - for i := 0; i < n; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range n { + wg.Go(func() { migrateLegacySessionsIntoGlobalTopics(dir) - }() + }) } wg.Wait() @@ -4148,17 +4146,15 @@ func TestEnsureTopicIndexedConcurrentRunsHaveNoLostProjectUpdates(t *testing.T) const n = 12 start := make(chan struct{}) var wg sync.WaitGroup - for i := 0; i < n; i++ { - i := i - wg.Add(1) - go func() { - defer wg.Done() + for i := range n { + + wg.Go(func() { <-start topicID := fmt.Sprintf("topic_recovered_%02d", i) if err := ensureTopicIndexed("project", projectRoot, topicID, fmt.Sprintf("Recovered %02d", i), topicTitleSourceManual); err != nil { t.Errorf("ensure topic indexed: %v", err) } - }() + }) } close(start) wg.Wait() @@ -4171,7 +4167,7 @@ func TestEnsureTopicIndexedConcurrentRunsHaveNoLostProjectUpdates(t *testing.T) for _, child := range nodes[0].Children { got[child.TopicID] = true } - for i := 0; i < n; i++ { + for i := range n { topicID := fmt.Sprintf("topic_recovered_%02d", i) if !got[topicID] { t.Fatalf("concurrent topic index recovery lost %q; children=%#v", topicID, nodes[0].Children) diff --git a/desktop/task_monitor_test.go b/desktop/task_monitor_test.go index ec218d7911..d047e8602c 100644 --- a/desktop/task_monitor_test.go +++ b/desktop/task_monitor_test.go @@ -43,12 +43,10 @@ func TestTaskControlConcurrentInitializationReturnsOneService(t *testing.T) { const callers = 16 services := make(chan *taskmonitor.ControlService, callers) var wg sync.WaitGroup - for i := 0; i < callers; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range callers { + wg.Go(func() { services <- app.taskControl() - }() + }) } wg.Wait() close(services) diff --git a/desktop/temphelper_test.go b/desktop/temphelper_test.go index f6d09ca6a2..2115f1dac7 100644 --- a/desktop/temphelper_test.go +++ b/desktop/temphelper_test.go @@ -23,7 +23,7 @@ func robustTempDir(t *testing.T) string { } t.Cleanup(func() { var rmErr error - for i := 0; i < 100; i++ { + for range 100 { if rmErr = os.RemoveAll(dir); rmErr == nil { return } diff --git a/desktop/terminal_test.go b/desktop/terminal_test.go index aef1b92964..5f1f1468bc 100644 --- a/desktop/terminal_test.go +++ b/desktop/terminal_test.go @@ -291,13 +291,13 @@ func TestTerminalManagerCountsConcurrentStartsTowardLimit(t *testing.T) { type result struct{ err error } results := make(chan result, maxTerminalsPerWorkspace+1) - for i := 0; i < maxTerminalsPerWorkspace+1; i++ { + for range maxTerminalsPerWorkspace + 1 { go func() { _, err := manager.create("tab", "workspace", ".", terminalCommand{path: "shell", label: "shell"}) results <- result{err: err} }() } - for i := 0; i < maxTerminalsPerWorkspace; i++ { + for range maxTerminalsPerWorkspace { select { case <-entered: case <-time.After(time.Second): @@ -307,7 +307,7 @@ func TestTerminalManagerCountsConcurrentStartsTowardLimit(t *testing.T) { close(release) succeeded, limited := 0, 0 - for i := 0; i < maxTerminalsPerWorkspace+1; i++ { + for range maxTerminalsPerWorkspace + 1 { result := <-results switch { case result.err == nil: diff --git a/desktop/theme_image.go b/desktop/theme_image.go index 6dc529c58e..c75467c8fd 100644 --- a/desktop/theme_image.go +++ b/desktop/theme_image.go @@ -131,12 +131,12 @@ func decodeDataURLImage(dataURL string) (filename string, data []byte, err error } // data:[][;base64], rest := dataURL[len(prefix):] - comma := strings.IndexByte(rest, ',') - if comma < 0 { + before, after, ok := strings.Cut(rest, ",") + if !ok { return "", nil, fmt.Errorf("invalid data URL") } - meta := rest[:comma] - payload := rest[comma+1:] + meta := before + payload := after parts := strings.Split(meta, ";") mime := strings.TrimSpace(parts[0]) base64Enc := false diff --git a/desktop/theme_pack.go b/desktop/theme_pack.go index a15ab60cb2..ad047683aa 100644 --- a/desktop/theme_pack.go +++ b/desktop/theme_pack.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "fmt" + "maps" "path/filepath" "regexp" "strings" @@ -84,7 +85,7 @@ type ThemePackManifest struct { Recipes ThemePackRecipes `json:"recipes"` Background *ThemePackBackground `json:"background,omitempty"` TaskBackground *ThemePackSceneBackground `json:"taskBackground,omitempty"` - Extra map[string]interface{} `json:"-"` // rejected on parse when present as unknown top-level + Extra map[string]any `json:"-"` // rejected on parse when present as unknown top-level } // ThemePackTokens holds optional light/dark semantic color overrides. @@ -603,9 +604,7 @@ func copyStringMap(in map[string]string) map[string]string { return nil } out := make(map[string]string, len(in)) - for k, v := range in { - out[k] = v - } + maps.Copy(out, in) return out } diff --git a/desktop/theme_pack_test.go b/desktop/theme_pack_test.go index ee63240924..43a3a95ef9 100644 --- a/desktop/theme_pack_test.go +++ b/desktop/theme_pack_test.go @@ -631,8 +631,8 @@ func TestManifestJSONRoundTrip(t *testing.T) { func writeTestPNG(t *testing.T, path string, w, h int) string { t.Helper() img := image.NewRGBA(image.Rect(0, 0, w, h)) - for y := 0; y < h; y++ { - for x := 0; x < w; x++ { + for y := range h { + for x := range w { img.Set(x, y, color.RGBA{R: uint8(x), G: uint8(y), B: 40, A: 255}) } } diff --git a/desktop/theme_plugin_test.go b/desktop/theme_plugin_test.go index 315e536f90..c2bda84f59 100644 --- a/desktop/theme_plugin_test.go +++ b/desktop/theme_plugin_test.go @@ -45,8 +45,8 @@ func testPluginThemeManifest(id, name string) *ThemePackManifest { func testPNGBytes(t *testing.T) []byte { t.Helper() img := image.NewRGBA(image.Rect(0, 0, 8, 8)) - for y := 0; y < 8; y++ { - for x := 0; x < 8; x++ { + for y := range 8 { + for x := range 8 { img.Set(x, y, color.RGBA{R: uint8(x * 30), G: uint8(y * 30), B: 40, A: 255}) } } diff --git a/desktop/tray_loop_windows_test.go b/desktop/tray_loop_windows_test.go index 8924ae9470..d7a0d2bfe5 100644 --- a/desktop/tray_loop_windows_test.go +++ b/desktop/tray_loop_windows_test.go @@ -13,7 +13,7 @@ func TestDesktopTrayLoopRunsOnLockedOSThread(t *testing.T) { done := make(chan struct{}) runDesktopTrayLoop(func() { first := windows.GetCurrentThreadId() - for i := 0; i < 100; i++ { + for range 100 { runtime.Gosched() } if got := windows.GetCurrentThreadId(); got != first { diff --git a/desktop/updater_deb_linux.go b/desktop/updater_deb_linux.go index 6bbd0de88e..166c28b543 100644 --- a/desktop/updater_deb_linux.go +++ b/desktop/updater_deb_linux.go @@ -145,7 +145,7 @@ func helperErrorMessage(stdout, stderr []byte) string { } // Strip protocol lines from stderr diagnostics. var kept []string - for _, line := range strings.Split(string(stderr), "\n") { + for line := range strings.SplitSeq(string(stderr), "\n") { if _, ok := parseHelperPhaseLine(line); ok { continue } diff --git a/desktop/updater_install_linux.go b/desktop/updater_install_linux.go index b5ab80e404..37d3334175 100644 --- a/desktop/updater_install_linux.go +++ b/desktop/updater_install_linux.go @@ -112,7 +112,7 @@ func isDpkgOwnedReasonix(absPath string) bool { return false } // dpkg-query may return multiple lines for diversions; require an exact package hit. - for _, raw := range strings.Split(line, "\n") { + for raw := range strings.SplitSeq(line, "\n") { raw = strings.TrimSpace(raw) pkg, path, ok := strings.Cut(raw, ":") if !ok { diff --git a/desktop/updater_mac.go b/desktop/updater_mac.go index 07038464c9..e5c2154bdb 100644 --- a/desktop/updater_mac.go +++ b/desktop/updater_mac.go @@ -611,7 +611,7 @@ func completeMacHandoffHandshake(cfg macUpdateHandoffConfig) error { } func retainMacHandoffNode(path, suffix string) (string, error) { - for attempt := 0; attempt < 16; attempt++ { + for attempt := range 16 { retained := fmt.Sprintf( "%s.%s-%d-%d", path, @@ -636,7 +636,7 @@ func cleanupOwnedMacUpdateDirectory(path string, owner os.FileInfo) error { if strings.TrimSpace(path) == "" || owner == nil || !owner.IsDir() { return fmt.Errorf("macOS update cleanup identity is incomplete") } - for attempt := 0; attempt < 16; attempt++ { + for attempt := range 16 { cleanup := fmt.Sprintf("%s.reasonix-cleanup-%d-%d", path, time.Now().UTC().UnixNano(), attempt) err := unix.RenameatxNp(unix.AT_FDCWD, path, unix.AT_FDCWD, cleanup, unix.RENAME_EXCL) if err != nil { diff --git a/desktop/updater_test.go b/desktop/updater_test.go index ed277b5cd7..ba69c40801 100644 --- a/desktop/updater_test.go +++ b/desktop/updater_test.go @@ -93,7 +93,7 @@ func TestValidateAssetInstallLayout(t *testing.T) { } func TestUpdaterWailsMethodContracts(t *testing.T) { - appType := reflect.TypeOf((*App)(nil)) + appType := reflect.TypeFor[*App]() tests := []struct { name string numIn int @@ -1256,9 +1256,9 @@ func fastRetry(t *testing.T) { func TestDownloadRecoversFromMidStreamReset(t *testing.T) { fastRetry(t) const body = "complete-installer-bytes" - var calls int32 + var calls atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - if atomic.AddInt32(&calls, 1) < int32(downloadAttempts) { + if calls.Add(1) < int32(downloadAttempts) { // Mid-stream reset: promise 100 bytes, send a few, drop the socket — // the client's body read fails with unexpected EOF, exactly the CN-IPv6 // "forcibly closed" case the retry exists for. @@ -1283,16 +1283,16 @@ func TestDownloadRecoversFromMidStreamReset(t *testing.T) { if string(data) != body { t.Fatalf("got %q, want %q", data, body) } - if n := atomic.LoadInt32(&calls); n != int32(downloadAttempts) { + if n := calls.Load(); n != int32(downloadAttempts) { t.Fatalf("made %d attempts, want %d", n, downloadAttempts) } } func TestDownloadGivesUpAfterCap(t *testing.T) { fastRetry(t) - var calls int32 + var calls atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - atomic.AddInt32(&calls, 1) + calls.Add(1) conn, _, err := w.(http.Hijacker).Hijack() if err != nil { t.Errorf("hijack: %v", err) @@ -1305,7 +1305,7 @@ func TestDownloadGivesUpAfterCap(t *testing.T) { if _, err := download(context.Background(), srv.Client(), nil, srv.URL, 0, nil); err == nil { t.Fatal("download should fail after exhausting retries") } - if n := atomic.LoadInt32(&calls); n != int32(downloadAttempts) { + if n := calls.Load(); n != int32(downloadAttempts) { t.Fatalf("made %d attempts, want %d", n, downloadAttempts) } } @@ -1329,10 +1329,10 @@ func TestDownloadResumesWithRange(t *testing.T) { fastRetry(t) full := bytes.Repeat([]byte("0123456789"), 50) // 500 bytes const cut = 200 - var calls int32 + var calls atomic.Int32 rangeCh := make(chan string, 4) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if atomic.AddInt32(&calls, 1) == 1 { + if calls.Add(1) == 1 { // First attempt: promise the whole file, send a prefix, drop the socket. conn, bw, err := w.(http.Hijacker).Hijack() if err != nil { @@ -1379,9 +1379,9 @@ func TestDownloadFallsBackToSecondClient(t *testing.T) { primary := &http.Client{Transport: rtFunc(func(*http.Request) (*http.Response, error) { return nil, errors.New("connection reset (ipv6)") })} - var fbCalls int32 + var fbCalls atomic.Int32 fallback := &http.Client{Transport: rtFunc(func(*http.Request) (*http.Response, error) { - atomic.AddInt32(&fbCalls, 1) + fbCalls.Add(1) return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(body)), @@ -1397,7 +1397,7 @@ func TestDownloadFallsBackToSecondClient(t *testing.T) { if string(data) != body { t.Fatalf("got %q, want %q", data, body) } - if atomic.LoadInt32(&fbCalls) == 0 { + if fbCalls.Load() == 0 { t.Fatal("fallback client was never used after the primary failed") } } @@ -1480,9 +1480,9 @@ func TestFetchBytesFallbackEscapesStalledPrimary(t *testing.T) { func TestFetchBytesDoesNotRetryPermanentHTTPStatus(t *testing.T) { fastRetry(t) - var calls int32 + var calls atomic.Int32 client := &http.Client{Transport: rtFunc(func(*http.Request) (*http.Response, error) { - atomic.AddInt32(&calls, 1) + calls.Add(1) return &http.Response{ StatusCode: http.StatusForbidden, Status: "403 Forbidden", @@ -1494,7 +1494,7 @@ func TestFetchBytesDoesNotRetryPermanentHTTPStatus(t *testing.T) { if _, err := fetchBytes(context.Background(), client, "https://example.invalid/latest.json"); err == nil { t.Fatal("fetchBytes should return a permanent HTTP error") } - if got := atomic.LoadInt32(&calls); got != 1 { + if got := calls.Load(); got != 1 { t.Fatalf("permanent HTTP error made %d requests, want 1", got) } } @@ -1502,9 +1502,9 @@ func TestFetchBytesDoesNotRetryPermanentHTTPStatus(t *testing.T) { func TestFetchBytesRejectsOversizeResponsesWithoutRetry(t *testing.T) { fastRetry(t) t.Run("declared content length", func(t *testing.T) { - var calls int32 + var calls atomic.Int32 client := &http.Client{Transport: rtFunc(func(*http.Request) (*http.Response, error) { - atomic.AddInt32(&calls, 1) + calls.Add(1) return &http.Response{ StatusCode: http.StatusOK, Status: "200 OK", @@ -1523,7 +1523,7 @@ func TestFetchBytesRejectsOversizeResponsesWithoutRetry(t *testing.T) { ); !errors.Is(err, errUpdateResponseTooLarge) { t.Fatalf("declared oversize error = %v, want errUpdateResponseTooLarge", err) } - if got := atomic.LoadInt32(&calls); got != 1 { + if got := calls.Load(); got != 1 { t.Fatalf("declared oversize response made %d requests, want 1", got) } }) diff --git a/desktop/window_state_test.go b/desktop/window_state_test.go index e8b7a91b93..30996329c1 100644 --- a/desktop/window_state_test.go +++ b/desktop/window_state_test.go @@ -187,7 +187,7 @@ func TestSaveWindowStateConcurrentReports(t *testing.T) { app := NewApp() var wg sync.WaitGroup - for i := 0; i < 32; i++ { + for i := range 32 { wg.Add(1) go func(i int) { defer wg.Done() diff --git a/desktop/workspace_changes.go b/desktop/workspace_changes.go index 66de5a93b0..d5ed95a7de 100644 --- a/desktop/workspace_changes.go +++ b/desktop/workspace_changes.go @@ -351,7 +351,7 @@ func workspaceCurrentText(path string) (string, bool, bool, error) { func tallyUnifiedPatch(patch string) (added, removed int) { inHunk := false - for _, line := range strings.Split(patch, "\n") { + for line := range strings.SplitSeq(patch, "\n") { switch { case strings.HasPrefix(line, "@@"): inHunk = true diff --git a/desktop/workspace_test.go b/desktop/workspace_test.go index a9f99326da..4940d8fc01 100644 --- a/desktop/workspace_test.go +++ b/desktop/workspace_test.go @@ -568,7 +568,7 @@ func BenchmarkDesktopSessionDir(b *testing.B) { b.Fatal(err) } b.ReportAllocs() - for i := 0; i < b.N; i++ { + for range b.N { if desktopSessionDir(root) == "" { b.Fatal("empty session dir") } @@ -950,7 +950,7 @@ func TestMediaTokenMaxEviction(t *testing.T) { // Fill beyond max to trigger eviction of oldest. var oldestToken string - for i := 0; i < mediaTokenMax+1; i++ { + for i := range mediaTokenMax + 1 { tok := store.create(dir+"/test.png", "test.png", "image/png", "image", 4, time.Time{}) if i == 0 { oldestToken = tok diff --git a/internal/acp/protocol_test.go b/internal/acp/protocol_test.go index 9cb96bbbdf..87c59b204b 100644 --- a/internal/acp/protocol_test.go +++ b/internal/acp/protocol_test.go @@ -116,7 +116,7 @@ func TestNewSessionID(t *testing.T) { func TestNewSessionIDUnique(t *testing.T) { seen := map[string]bool{} - for i := 0; i < 100; i++ { + for range 100 { id, err := newSessionID() if err != nil { t.Fatalf("newSessionID: %v", err) diff --git a/internal/acp/server.go b/internal/acp/server.go index 07dda83615..07900ba171 100644 --- a/internal/acp/server.go +++ b/internal/acp/server.go @@ -208,18 +208,14 @@ func (c *Conn) dispatch(ctx context.Context, line []byte) { hasID := len(in.ID) > 0 switch { case in.Method != "" && hasID: - c.wg.Add(1) - go func() { - defer c.wg.Done() + c.wg.Go(func() { c.serveRequest(ctx, in.ID, in.Method, in.Params) - }() + }) case in.Method != "" && !hasID: if h := c.notH[in.Method]; h != nil { - c.wg.Add(1) - go func() { - defer c.wg.Done() + c.wg.Go(func() { h(ctx, in.Params) - }() + }) } case in.Method == "" && hasID: c.resolve(in) diff --git a/internal/acp/service.go b/internal/acp/service.go index 53681d2199..098662fce5 100644 --- a/internal/acp/service.go +++ b/internal/acp/service.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "log/slog" + "maps" "os" "path/filepath" "sort" @@ -2955,9 +2956,7 @@ func mapString(in map[string]string) map[string]string { return nil } out := make(map[string]string, len(in)) - for k, v := range in { - out[k] = v - } + maps.Copy(out, in) return out } diff --git a/internal/acp/service_lock_test.go b/internal/acp/service_lock_test.go index 9f09894914..c537bf956d 100644 --- a/internal/acp/service_lock_test.go +++ b/internal/acp/service_lock_test.go @@ -411,7 +411,7 @@ func TestACPCtrlReadPathsDoNotRaceWithRebuild(t *testing.T) { done := make(chan struct{}) go func() { defer close(done) - for i := 0; i < rebuilds; i++ { + for i := range rebuilds { if err := svc.rebuildSession(context.Background(), sess, SessionConfigState{Model: models[i%len(models)]}, []sessionConfigDelta{{axis: "model", model: models[i%len(models)]}}); err != nil { t.Errorf("rebuildSession %d: %v", i, err) return diff --git a/internal/acp/status.go b/internal/acp/status.go index 4745277624..14644765d0 100644 --- a/internal/acp/status.go +++ b/internal/acp/status.go @@ -6,6 +6,7 @@ import ( "errors" "math" "runtime" + "slices" "strings" "sync" @@ -710,9 +711,9 @@ func finalAssistantSummary(ctrl acpController) string { return "" } history := ctrl.History() - for i := len(history) - 1; i >= 0; i-- { - if history[i].Role == provider.RoleAssistant && strings.TrimSpace(history[i].Content) != "" { - return history[i].Content + for _, v := range slices.Backward(history) { + if v.Role == provider.RoleAssistant && strings.TrimSpace(v.Content) != "" { + return v.Content } } return "" diff --git a/internal/agent/agent.go b/internal/agent/agent.go index bf55fda04a..44a2f7b425 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -1187,10 +1187,7 @@ func New(prov provider.Provider, tools *tool.Registry, session *Session, opts Op } else { maxSubagentDepth = NormalizeMaxSubagentDepth(maxSubagentDepth) } - subagentDepth := opts.SubagentDepth - if subagentDepth < 0 { - subagentDepth = 0 - } + subagentDepth := max(opts.SubagentDepth, 0) reasoningByteLimit := opts.ReasoningByteLimit if reasoningByteLimit == 0 { reasoningByteLimit = defaultReasoningByteLimit @@ -3297,7 +3294,7 @@ launch: <-sem break } - i := i + wg.Add(1) ranUntil = i + 1 go func() { @@ -4016,8 +4013,8 @@ func (a *Agent) toolReadOnly(name string) bool { // firstLine returns s up to its first newline — a one-line failure summary for // the display Err, while the full error stays in the model-facing output. func firstLine(s string) string { - if i := strings.IndexByte(s, '\n'); i >= 0 { - return s[:i] + if before, _, ok := strings.Cut(s, "\n"); ok { + return before } return s } diff --git a/internal/agent/branch.go b/internal/agent/branch.go index 09a5d59090..7c4a124abc 100644 --- a/internal/agent/branch.go +++ b/internal/agent/branch.go @@ -302,10 +302,7 @@ func MarkSessionInFlightTurn(sessionPath string, startMessageIndex int, preserve // what lets crash recovery relocate the turn after an in-turn compaction has // rewritten its original message index. func SetSessionInFlightTurn(sessionPath string, marker InFlightTurnMeta) error { - startMessageIndex := marker.StartMessageIndex - if startMessageIndex < 0 { - startMessageIndex = 0 - } + startMessageIndex := max(marker.StartMessageIndex, 0) // The sidecar is read-modify-write; the per-path save lock keeps concurrent // writers (autosave's UpdateSessionMeta, listing backfill) from dropping // each other's fields. diff --git a/internal/agent/cache_shape.go b/internal/agent/cache_shape.go index 3b86acb2c4..59d6e100b0 100644 --- a/internal/agent/cache_shape.go +++ b/internal/agent/cache_shape.go @@ -26,7 +26,7 @@ type PrefixShape struct { // every call site, while still assigning to event.Event.CacheDiagnostics. type CacheDiagnostics = event.CacheDiagnostics -func shortHash(v interface{}) string { +func shortHash(v any) string { b, _ := json.Marshal(v) h := sha256.Sum256(b) return fmt.Sprintf("%x", h[:8]) @@ -39,7 +39,7 @@ func CaptureShape(systemPrompt string, schemas []provider.ToolSchema, rewriteVer return PrefixShape{ SystemHash: shortHash(systemPrompt), ToolsHash: shortHash(string(toolsJSON)), - PrefixHash: shortHash(map[string]interface{}{ + PrefixHash: shortHash(map[string]any{ "system": systemPrompt, "tools": string(toolsJSON), }), diff --git a/internal/agent/cachehit_e2e_test.go b/internal/agent/cachehit_e2e_test.go index 013d60854b..0680db9917 100644 --- a/internal/agent/cachehit_e2e_test.go +++ b/internal/agent/cachehit_e2e_test.go @@ -199,7 +199,7 @@ func TestCacheHitClimbsWithoutCompaction(t *testing.T) { a, sink := newAgent(t, srv.URL, mock.tools(), 0 /*no compaction*/, 0) const turns = 14 - for i := 0; i < turns; i++ { + for i := range turns { userMsg := "Turn " + fmt.Sprint(i) + ": " + strings.Repeat("please consider this requirement. ", 6) if err := a.Run(context.Background(), userMsg); err != nil { t.Fatalf("Run %d: %v", i, err) @@ -286,7 +286,7 @@ func TestReasoningRoundTripCost(t *testing.T) { defer srv.Close() a, sink := newAgent(t, srv.URL, mock.tools(), 0, 0) const turns = 12 - for i := 0; i < turns; i++ { + for i := range turns { if err := a.Run(context.Background(), strings.Repeat("please consider this requirement. ", 6)); err != nil { t.Fatalf("Run %d: %v", i, err) } @@ -329,7 +329,7 @@ func TestSessionAggregateCacheRate(t *testing.T) { a, sink := newAgent(t, srv.URL, mock.tools(), 0, 0) const turns = 8 - for i := 0; i < turns; i++ { + for i := range turns { if err := a.Run(context.Background(), strings.Repeat("please consider this requirement. ", 6)); err != nil { t.Fatalf("Run %d: %v", i, err) } @@ -409,7 +409,7 @@ func TestReleaseCacheHitGuard(t *testing.T) { name: "mixed-message-sizes", run: func(t *testing.T) []int { msgs := make([]string, 0, 20) - for i := 0; i < 20; i++ { + for i := range 20 { repeats := 4 if i%3 == 2 { repeats = 20 @@ -482,7 +482,7 @@ func cacheCurve(t *testing.T, mock *mockDeepSeek, turns int) []int { func repeatedMessages(turns, repeats int) []string { msgs := make([]string, 0, turns) - for i := 0; i < turns; i++ { + for i := range turns { msgs = append(msgs, "Turn "+fmt.Sprint(i)+": "+strings.Repeat("please consider this requirement. ", repeats)) } return msgs diff --git a/internal/agent/cancel_test.go b/internal/agent/cancel_test.go index 94930f378d..b56fd0fd7a 100644 --- a/internal/agent/cancel_test.go +++ b/internal/agent/cancel_test.go @@ -130,7 +130,7 @@ func (closedStreamProvider) Stream(context.Context, provider.Request) (<-chan pr } func TestCanceledContextClosedProviderStreamReturnsCancel(t *testing.T) { - for i := 0; i < 100; i++ { + for i := range 100 { ctx, cancel := context.WithCancel(context.Background()) cancel() @@ -574,7 +574,7 @@ func TestCancelInsideLargeParallelBatchStopsSchedulingNewTools(t *testing.T) { reg.Add(trackingTool{name: "readonly_tracking", readOnly: true}) var calls []provider.ToolCall - for i := 0; i < 12; i++ { + for i := range 12 { calls = append(calls, provider.ToolCall{ ID: fmt.Sprintf("call-%02d", i), Name: "readonly_tracking", diff --git a/internal/agent/compact_loop_e2e_test.go b/internal/agent/compact_loop_e2e_test.go index 691da1f14e..e57dac5afb 100644 --- a/internal/agent/compact_loop_e2e_test.go +++ b/internal/agent/compact_loop_e2e_test.go @@ -109,7 +109,7 @@ func compactionsPerTurn(t *testing.T, windowTok int, blob, finalText string, tur }) perTurn = make([]int, turns) - for i := 0; i < turns; i++ { + for i := range turns { before := started if err := a.Run(context.Background(), fmt.Sprintf("turn %d: keep going, continue the work", i)); err != nil { t.Fatalf("Run %d: %v", i, err) diff --git a/internal/agent/coordinator.go b/internal/agent/coordinator.go index 1f8882aef1..f22520dd73 100644 --- a/internal/agent/coordinator.go +++ b/internal/agent/coordinator.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "slices" "strings" "time" @@ -521,17 +522,17 @@ func plannerPlanRequestsApproval(plan string) bool { } // Match per line so a nearby negation ("无需等待用户批准", "no need to wait // for approval") exempts only its own phrase, not the whole plan. - for _, rawLine := range strings.Split(lower, "\n") { + for rawLine := range strings.SplitSeq(lower, "\n") { line := strings.TrimSpace(rawLine) if line == "" { continue } for _, phrase := range plannerApprovalPhrases { - idx := strings.Index(line, phrase) - if idx < 0 { + before, _, ok := strings.Cut(line, phrase) + if !ok { continue } - if approvalMentionNegated(line[:idx]) { + if approvalMentionNegated(before) { continue } return true @@ -633,7 +634,7 @@ func parsePlannerAskBlock(plan string) (event.AskQuestion, bool) { block := plan[start+len(plannerAskStartMarker) : end] var question string var options []event.AskOption - for _, raw := range strings.Split(block, "\n") { + for raw := range strings.SplitSeq(block, "\n") { line := strings.TrimSpace(raw) if line == "" { continue @@ -671,8 +672,8 @@ func parsePlannerAskBlock(plan string) (event.AskQuestion, bool) { func plannerQuestionPrompt(plan string) string { lines := strings.Split(plan, "\n") - for i := len(lines) - 1; i >= 0; i-- { - line := strings.TrimSpace(strings.Trim(lines[i], "-* \t")) + for _, v := range slices.Backward(lines) { + line := strings.TrimSpace(strings.Trim(v, "-* \t")) if line == "" { continue } @@ -780,8 +781,8 @@ func isNoOpPlan(plan string) bool { func lastNonEmptyLine(s string) string { lines := strings.Split(s, "\n") - for i := len(lines) - 1; i >= 0; i-- { - if t := strings.TrimSpace(lines[i]); t != "" { + for _, v := range slices.Backward(lines) { + if t := strings.TrimSpace(v); t != "" { return t } } @@ -1076,11 +1077,11 @@ func HandoffTask(s string) string { return s } const header = "Original task:\n" - i := strings.Index(trimmed, header) - if i < 0 { + _, after, ok := strings.Cut(trimmed, header) + if !ok { return s } - rest := trimmed[i+len(header):] + rest := after if j := strings.Index(rest, "\n\nPlanner output:"); j >= 0 { rest = rest[:j] } diff --git a/internal/agent/coordinator_test.go b/internal/agent/coordinator_test.go index 572420680f..1741a1b0ef 100644 --- a/internal/agent/coordinator_test.go +++ b/internal/agent/coordinator_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "reasonix/internal/event" + "slices" "strings" "testing" @@ -43,9 +44,9 @@ func (m *mockProvider) Stream(ctx context.Context, req provider.Request) (<-chan } func lastUser(req provider.Request) string { - for i := len(req.Messages) - 1; i >= 0; i-- { - if req.Messages[i].Role == provider.RoleUser { - return req.Messages[i].Content + for _, v := range slices.Backward(req.Messages) { + if v.Role == provider.RoleUser { + return v.Content } } return "" @@ -1274,17 +1275,12 @@ func toolSchemaNames(schemas []provider.ToolSchema) []string { } func contains(items []string, want string) bool { - for _, item := range items { - if item == want { - return true - } - } - return false + return slices.Contains(items, want) } func BenchmarkPlannerToolRegistry(b *testing.B) { parentReg := tool.NewRegistry() - for i := 0; i < 200; i++ { + for i := range 200 { parentReg.Add(coordinatorTestTool{ name: fmt.Sprintf("tool_%03d", i), readOnly: i%3 != 0, @@ -1294,7 +1290,7 @@ func BenchmarkPlannerToolRegistry(b *testing.B) { parentReg.Add(coordinatorTestTool{name: "write_file", readOnly: false}) b.ReportAllocs() - for i := 0; i < b.N; i++ { + for range b.N { reg := PlannerToolRegistry(parentReg) if reg.Len() == 0 { b.Fatal("planner registry should retain read-only research tools") @@ -1727,7 +1723,7 @@ func TestCoordinatorHandoffSurvivesPlannerCompaction(t *testing.T) { // below) its pre-turn length, which is what strands a boundary based on // the pre-turn message count. filler := strings.Repeat("planner history filler. ", 150) - for i := 0; i < 3; i++ { + for range 3 { plannerSess.Add(provider.Message{Role: provider.RoleUser, Content: filler}) plannerSess.Add(provider.Message{Role: provider.RoleAssistant, Content: filler}) } @@ -1873,7 +1869,7 @@ func TestCoordinatorFailedTurnRollbackKeepsCompaction(t *testing.T) { executor := New(exec, tool.NewRegistry(), NewSession("exec-sys"), Options{}, event.Discard) plannerSess := NewSession("planner-sys") filler := strings.Repeat("planner history filler. ", 150) - for i := 0; i < 3; i++ { + for range 3 { plannerSess.Add(provider.Message{Role: provider.RoleUser, Content: filler}) plannerSess.Add(provider.Message{Role: provider.RoleAssistant, Content: filler}) } diff --git a/internal/agent/empty_final_test.go b/internal/agent/empty_final_test.go index a969fc5b9b..9b43876ad6 100644 --- a/internal/agent/empty_final_test.go +++ b/internal/agent/empty_final_test.go @@ -180,7 +180,7 @@ func BenchmarkHasVisibleFinalAnswer(b *testing.B) { b.Run(tc.name, func(b *testing.B) { b.ReportAllocs() var got bool - for i := 0; i < b.N; i++ { + for range b.N { got = hasVisibleFinalAnswer(tc.text) } _ = got diff --git a/internal/agent/fleet.go b/internal/agent/fleet.go index 0fdf8c5b71..4b5a30210c 100644 --- a/internal/agent/fleet.go +++ b/internal/agent/fleet.go @@ -323,9 +323,7 @@ func (f *FleetTool) runFleet(ctx context.Context, sink event.Sink, specs []Profi }, }) - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { // Each fleet item runs as its own task-shaped execution so // transcripts, evidence, and scheduler claims stay independent. itemCtx := withCallContext(ctx, subID, subSinkFor(subID, sink), nil, false) @@ -350,7 +348,7 @@ func (f *FleetTool) runFleet(ctx context.Context, sink event.Sink, specs []Profi }) } doneCh <- res - }() + }) } // Mark only genuinely unstarted items skipped. Started items always publish diff --git a/internal/agent/fleet_test.go b/internal/agent/fleet_test.go index 25e16349a5..d9320e7c54 100644 --- a/internal/agent/fleet_test.go +++ b/internal/agent/fleet_test.go @@ -356,7 +356,7 @@ func TestFleetParallelDisjointWriters(t *testing.T) { f := NewFleetTool(task) tasks := make([]map[string]any, 0, 4) - for i := 0; i < 4; i++ { + for i := range 4 { path := filepath.Join("docs", "f"+string(rune('0'+i))+".md") tasks = append(tasks, map[string]any{ "prompt": "handle " + path, diff --git a/internal/agent/goal_display.go b/internal/agent/goal_display.go index c8afd55d0f..4705c21b61 100644 --- a/internal/agent/goal_display.go +++ b/internal/agent/goal_display.go @@ -56,11 +56,11 @@ func StripAutoResearchEvidenceBlocks(text string) string { } b.WriteString(rest[:start]) afterOpen := rest[start+len(autoResearchEvidenceOpen):] - end := strings.Index(afterOpen, autoResearchEvidenceClose) - if end < 0 { + _, after, ok := strings.Cut(afterOpen, autoResearchEvidenceClose) + if !ok { return strings.TrimSpace(b.String()) } - rest = afterOpen[end+len(autoResearchEvidenceClose):] + rest = after } } diff --git a/internal/agent/interrupted_recovery.go b/internal/agent/interrupted_recovery.go index c0f1db1d05..4410075e4d 100644 --- a/internal/agent/interrupted_recovery.go +++ b/internal/agent/interrupted_recovery.go @@ -3,6 +3,7 @@ package agent import ( "fmt" "html" + "slices" "strings" "reasonix/internal/provider" @@ -24,8 +25,8 @@ func (a *Agent) pendingInterruptedRecovery() *provider.InterruptedTurnRecovery { return nil } msgs := a.session.Snapshot() - for i := len(msgs) - 1; i >= 0; i-- { - m := msgs[i] + for _, v := range slices.Backward(msgs) { + m := v if m.LocalOnly && m.InterruptedTurn != nil && m.InterruptedTurn.Pending { copy := *m.InterruptedTurn copy.CompletedTools = append([]provider.InterruptedToolSummary(nil), copy.CompletedTools...) diff --git a/internal/agent/loop_e2e_test.go b/internal/agent/loop_e2e_test.go index be01839ec0..20dcb10de1 100644 --- a/internal/agent/loop_e2e_test.go +++ b/internal/agent/loop_e2e_test.go @@ -417,7 +417,7 @@ func TestRunStreamRetryRequestCountIsLinearNotTriangular(t *testing.T) { func TestRunExhaustedStreamRetriesPersistPendingLocalOnly(t *testing.T) { interrupted := &provider.StreamInterruptedError{Err: errors.New("eof"), Reason: provider.StreamInterruptPrematureEOF} turns := make([]testutil.Turn, 0, maxSamplingAttempts) - for i := 0; i < maxSamplingAttempts; i++ { + for range maxSamplingAttempts { turns = append(turns, testutil.Turn{Text: "half", ChunkError: interrupted}) } mp := testutil.NewMock("m", turns...) @@ -1026,7 +1026,7 @@ func TestHealthyToolCallReasoningRetriesTransientStateWriteFailure(t *testing.T) t.Fatal(err) } permissionsRestored = true - for healthy := 0; healthy < missingReasoningHealthyResolveStreak-1; healthy++ { + for healthy := range missingReasoningHealthyResolveStreak - 1 { if missing, retry := a.observeMissingToolCallReasoning(calls, "healthy reasoning"); missing || retry { t.Fatalf("healthy recovery observation %d = missing:%v retry:%v, want false/false", healthy+1, missing, retry) } diff --git a/internal/agent/parallel_tasks.go b/internal/agent/parallel_tasks.go index fb7ab516fa..809ce9b347 100644 --- a/internal/agent/parallel_tasks.go +++ b/internal/agent/parallel_tasks.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "slices" "strings" "sync" @@ -177,9 +178,7 @@ func (p *ParallelTasksTool) Execute(ctx context.Context, args json.RawMessage) ( }, }) - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { modelRef, effortRef := p.taskTool.effectiveProfile(t.Model, t.Effort) itemCtx := withCallContext(ctx, subID, subSinkFor(subID, sink), nil, PlanModeFromContext(ctx)) // Route through TaskTool's unified runner so persisted parent sessions @@ -221,7 +220,7 @@ func (p *ParallelTasksTool) Execute(ctx context.Context, args json.RawMessage) ( }) answer, ref := splitSubagentRunResult(output) doneCh <- subResult{index: idx, output: answer, ref: ref} - }() + }) } markCancelled := func(err error) { @@ -300,10 +299,8 @@ func parallelGroupTerminalPhase(ctx context.Context, err error, statuses []paral if ctx.Err() != nil { return subagentPhaseCancelled } - for _, st := range statuses { - if st == parallelTaskFailed { - return subagentPhaseFailed - } + if slices.Contains(statuses, parallelTaskFailed) { + return subagentPhaseFailed } if err != nil { return subagentPhaseFailed diff --git a/internal/agent/parallel_tasks_test.go b/internal/agent/parallel_tasks_test.go index a30f73e699..f849ad1bc5 100644 --- a/internal/agent/parallel_tasks_test.go +++ b/internal/agent/parallel_tasks_test.go @@ -377,9 +377,9 @@ func (parallelLongResultProvider) Stream(_ context.Context, req provider.Request func subagentRefsFromText(text string) []string { var refs []string - for _, line := range strings.Split(text, "\n") { - if strings.HasPrefix(line, "Subagent reference: ") { - refs = append(refs, strings.TrimSpace(strings.TrimPrefix(line, "Subagent reference: "))) + for line := range strings.SplitSeq(text, "\n") { + if after, ok := strings.CutPrefix(line, "Subagent reference: "); ok { + refs = append(refs, strings.TrimSpace(after)) } } return refs diff --git a/internal/agent/prune_test.go b/internal/agent/prune_test.go index fc5cbff235..c15246317c 100644 --- a/internal/agent/prune_test.go +++ b/internal/agent/prune_test.go @@ -98,7 +98,7 @@ func TestPruneNeverRewritesLocalInterruptedDisplay(t *testing.T) { func TestSnipStaleToolResults(t *testing.T) { var lines []string - for i := 0; i < 1000; i++ { + for range 1000 { lines = append(lines, "line") } big := strings.Join(lines, "\n") @@ -376,7 +376,7 @@ func TestSnipUsesRegisteredToolHint(t *testing.T) { // 600 numbered lines; a SnipHinter keeping head=3, tail=2 must yield exactly // those boundary lines, which no default geometry would produce. var lines []string - for i := 0; i < 600; i++ { + for i := range 600 { lines = append(lines, fmt.Sprintf("L%d", i)) } content := strings.Join(lines, "\n") @@ -403,7 +403,7 @@ func TestSnipUsesRegisteredToolHint(t *testing.T) { func TestSnipFallsBackByReadOnlyTier(t *testing.T) { var lines []string - for i := 0; i < 600; i++ { + for i := range 600 { lines = append(lines, fmt.Sprintf("L%d", i)) } content := strings.Join(lines, "\n") diff --git a/internal/agent/reasoning_language.go b/internal/agent/reasoning_language.go index ce7f7b8e81..6187340098 100644 --- a/internal/agent/reasoning_language.go +++ b/internal/agent/reasoning_language.go @@ -242,11 +242,11 @@ func hasOpenTag(s, tag string) bool { func trimLeadingTransientBlock(content, tag string) (string, bool) { closeTag := "" - i := strings.Index(content, closeTag) - if i < 0 { + _, after, ok := strings.Cut(content, closeTag) + if !ok { return content, false } - return strings.TrimLeft(content[i+len(closeTag):], " \t\r\n"), true + return strings.TrimLeft(after, " \t\r\n"), true } // WithResponseLanguagePreference carries the runtime final-answer language diff --git a/internal/agent/reasoning_warn_state.go b/internal/agent/reasoning_warn_state.go index 0fd1f06b0b..003a76fdf4 100644 --- a/internal/agent/reasoning_warn_state.go +++ b/internal/agent/reasoning_warn_state.go @@ -214,13 +214,7 @@ func normalizeMissingReasoningIncident(incident missingReasoningIncident, now ti } func (incident missingReasoningIncident) lastEventUnixNano() int64 { - lastEvent := incident.LastMissingUnixNano - if incident.LastResolvedAtUnixNano > lastEvent { - lastEvent = incident.LastResolvedAtUnixNano - } - if incident.LastHealthyAtUnixNano > lastEvent { - lastEvent = incident.LastHealthyAtUnixNano - } + lastEvent := max(incident.LastHealthyAtUnixNano, max(incident.LastResolvedAtUnixNano, incident.LastMissingUnixNano)) return lastEvent } diff --git a/internal/agent/reasoning_warn_state_test.go b/internal/agent/reasoning_warn_state_test.go index 5df319dd5b..56bf6bfb29 100644 --- a/internal/agent/reasoning_warn_state_test.go +++ b/internal/agent/reasoning_warn_state_test.go @@ -406,14 +406,12 @@ func TestMissingReasoningWarnStateConcurrentSameIncidentWarnsOnce(t *testing.T) var warned atomic.Int64 var wg sync.WaitGroup for range 8 { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { <-start if newMissingReasoningWarnState(dir).claimAt(fingerprint, now) { warned.Add(1) } - }() + }) } close(start) wg.Wait() @@ -490,14 +488,12 @@ func TestMissingReasoningWarnStateConcurrentClaimsKeepEveryConfiguration(t *test var wg sync.WaitGroup for _, label := range labels { fingerprint := warningFingerprint(label) - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { <-start if !newMissingReasoningWarnState(dir).claimAt(fingerprint, now) { t.Errorf("fresh configuration %q did not claim its notice", label) } - }() + }) } close(start) wg.Wait() diff --git a/internal/agent/recovery_gc.go b/internal/agent/recovery_gc.go index 229f242cd0..a5b7a8e12e 100644 --- a/internal/agent/recovery_gc.go +++ b/internal/agent/recovery_gc.go @@ -457,7 +457,7 @@ func publishRecoveryTrashStage(dir, key, stageDir string) (string, error) { root := filepath.Join(dir, recoveryTrashDir) stem := strings.TrimSuffix(key, filepath.Ext(key)) stamp := time.Now().UTC().UnixMilli() - for i := 0; i < 1000; i++ { + for i := range 1000 { name := key if i > 0 { name = fmt.Sprintf("%s-recovery-%d-%d", stem, stamp, i) @@ -523,7 +523,7 @@ func reserveRecoveryTrashItemDir(dir, key string) (string, string, error) { return "", "", err } stem := strings.TrimSuffix(key, filepath.Ext(key)) - for i := 0; i < 1000; i++ { + for i := range 1000 { name := key if i > 0 { name = fmt.Sprintf("%s-recovery-%d-%d", stem, time.Now().UTC().UnixMilli(), i) diff --git a/internal/agent/repeat_guard_test.go b/internal/agent/repeat_guard_test.go index 9e1c41323e..f8fc7a682b 100644 --- a/internal/agent/repeat_guard_test.go +++ b/internal/agent/repeat_guard_test.go @@ -303,7 +303,7 @@ func TestRepeatGuardRetainsStaleEditFailuresAcrossGoalScope(t *testing.T) { TaskText: "fix prompt.txt", }) - for i := 0; i < 3; i++ { + for i := range 3 { if err := a.Run(ctx, "continue goal"); err != nil { t.Fatalf("Run %d: %v", i+1, err) } @@ -335,7 +335,7 @@ func TestRepeatGuardClearsOrdinaryWriteFailureAcrossGoalRuns(t *testing.T) { TaskText: "fix prompt.txt", }) - for i := 0; i < 3; i++ { + for i := range 3 { if err := a.Run(ctx, "continue goal"); err != nil { t.Fatalf("Run %d: %v", i+1, err) } diff --git a/internal/agent/run_loop.go b/internal/agent/run_loop.go index 790ce9fc16..351c2ad048 100644 --- a/internal/agent/run_loop.go +++ b/internal/agent/run_loop.go @@ -435,10 +435,7 @@ func (a *Agent) streamWithSamplingRecovery(ctx context.Context, turn int) stream before := provider.RequestAttemptCount(ctx) result := a.streamWithFrozen(ctx, turn, sink, &frozen, attemptID) after := provider.RequestAttemptCount(ctx) - delta := after - before - if delta < 0 { - delta = 0 - } + delta := max(after-before, 0) // httpRequests=0 means the provider does not use SendWithRetry // (extension/custom), or it failed before issuing an HTTP request. // Only overwrite RequestCount when the built-in counter observed POSTs; @@ -581,13 +578,7 @@ var streamRetrySleep = sleepStreamRetryBackoff // Returns false when ctx is cancelled during the wait. func sleepStreamRetryBackoff(ctx context.Context, attempt int) bool { // attempt is 1-based for the failed attempt about to be retried. - shift := attempt - 1 - if shift < 0 { - shift = 0 - } - if shift > 4 { - shift = 4 - } + shift := min(max(attempt-1, 0), 4) base := time.Duration(1< SessionRecoveryMaxDepth { + recoveryDepth := min(parentDepth+1, // A shutdown copy is allowed even when the ordinary conflict chain is // capped because losing the only in-memory transcript is worse than one // additional branch. Keep the saturated depth so later ordinary saves // still enforce the existing anti-cascade policy. - recoveryDepth = SessionRecoveryMaxDepth - } + SessionRecoveryMaxDepth) recoveryPath := recoverySessionPath(originalPath, digest) if shutdown { @@ -808,8 +807,8 @@ func recoveryParentStem(parent string) string { return "session" } sum := sha256.Sum256([]byte(parent)) - if idx := strings.Index(parent, "-recovery-"); idx >= 0 { - base := strings.Trim(parent[:idx], "-_. ") + if before, _, ok := strings.Cut(parent, "-recovery-"); ok { + base := strings.Trim(before, "-_. ") if base == "" { base = "session" } @@ -1265,8 +1264,8 @@ func resolvePathThroughExistingAncestor(path string) string { missing := make([]string, 0, 4) for { if resolved, err := filepath.EvalSymlinks(current); err == nil { - for i := len(missing) - 1; i >= 0; i-- { - resolved = filepath.Join(resolved, missing[i]) + for _, v := range slices.Backward(missing) { + resolved = filepath.Join(resolved, v) } return resolved } diff --git a/internal/agent/save_test.go b/internal/agent/save_test.go index 8fe8e596e4..8eb0af1701 100644 --- a/internal/agent/save_test.go +++ b/internal/agent/save_test.go @@ -2117,7 +2117,7 @@ func TestSaveSnapshotSameRevisionAllowsOwnedNonPrefixAppend(t *testing.T) { // Simulate recovery: load the saved transcript into a new session, then // add more messages. Every subsequent SaveSnapshot must succeed without a // diverged conflict while the persisted digest still proves ownership. - for turn := 0; turn < 5; turn++ { + for turn := range 5 { s, err := LoadSession(path) if err != nil { t.Fatalf("LoadSession turn %d: %v", turn, err) diff --git a/internal/agent/scheduler_test.go b/internal/agent/scheduler_test.go index 9b9dbd7e10..01c4a03491 100644 --- a/internal/agent/scheduler_test.go +++ b/internal/agent/scheduler_test.go @@ -16,10 +16,8 @@ func TestSchedulerTotalConcurrencyQueues(t *testing.T) { var wg sync.WaitGroup barrier := make(chan struct{}) - for i := 0; i < 4; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range 4 { + wg.Go(func() { release, err := s.Acquire(context.Background(), AcquireRequest{Writer: false}) if err != nil { t.Errorf("acquire: %v", err) @@ -35,7 +33,7 @@ func TestSchedulerTotalConcurrencyQueues(t *testing.T) { <-barrier started.Add(-1) release() - }() + }) } // Wait until at least 2 are running, then release them. diff --git a/internal/agent/session.go b/internal/agent/session.go index 68fabfce5b..bfd74c7353 100644 --- a/internal/agent/session.go +++ b/internal/agent/session.go @@ -86,6 +86,7 @@ func (s *Session) AddDecisionReceipt(receipt *provider.DecisionReceipt) { } s.mu.Lock() defer s.mu.Unlock() + //nolint:modernize // slices.Backward yields element copies; this body writes through the index. for i := len(s.Messages) - 1; i >= 0; i-- { if s.Messages[i].Role == provider.RoleUser && !s.Messages[i].LocalOnly { break @@ -123,6 +124,7 @@ func (s *Session) UpdateToolCallPreview(call provider.ToolCall) bool { } s.mu.Lock() defer s.mu.Unlock() + //nolint:modernize // slices.Backward yields element copies; this body writes through the index. for i := len(s.Messages) - 1; i >= 0; i-- { if s.Messages[i].Role != provider.RoleAssistant { continue @@ -159,6 +161,7 @@ func (s *Session) UpdateToolCallResolution(call provider.ToolCall) bool { } s.mu.Lock() defer s.mu.Unlock() + //nolint:modernize // slices.Backward yields element copies; this body writes through the index. for i := len(s.Messages) - 1; i >= 0; i-- { if s.Messages[i].Role != provider.RoleAssistant { continue diff --git a/internal/agent/session_concurrency_test.go b/internal/agent/session_concurrency_test.go index a3d5aee7b3..0df89afc79 100644 --- a/internal/agent/session_concurrency_test.go +++ b/internal/agent/session_concurrency_test.go @@ -26,26 +26,22 @@ func TestSessionConcurrentAddAndRead(t *testing.T) { var wg sync.WaitGroup // One writer mimicking the turn goroutine. - wg.Add(1) - go func() { - defer wg.Done() - for i := 0; i < appends; i++ { + wg.Go(func() { + for range appends { s.Add(provider.Message{Role: provider.RoleUser, Content: "msg"}) } - }() + }) // Many readers mimicking frontends polling history. - for r := 0; r < readers; r++ { - wg.Add(1) - go func() { - defer wg.Done() - for i := 0; i < reads; i++ { + for range readers { + wg.Go(func() { + for range reads { snap := s.Snapshot() for _, m := range snap { // iterate the copy: must never tear _ = m.Content } _ = s.HasContent() } - }() + }) } wg.Wait() diff --git a/internal/agent/session_events_test.go b/internal/agent/session_events_test.go index 018f751da5..a0a4206f93 100644 --- a/internal/agent/session_events_test.go +++ b/internal/agent/session_events_test.go @@ -16,7 +16,7 @@ import ( func sessionWithTurns(t *testing.T, path string, turns int) *Session { t.Helper() s := NewSession("sys") - for i := 0; i < turns; i++ { + for i := range turns { s.Add(provider.Message{Role: provider.RoleUser, Content: "prompt " + strings.Repeat("x", 8)}) s.Add(provider.Message{Role: provider.RoleAssistant, Content: "reply"}) if err := s.SaveSnapshot(path); err != nil { @@ -566,7 +566,7 @@ func TestEventLogCompactionBoundsGrowth(t *testing.T) { filler := strings.Repeat("y", 8<<10) // Repeated rewrites (each a full replace event) must not grow the log // without bound: once past the threshold the log folds to one event. - for i := 0; i < 60; i++ { + for i := range 60 { s.Replace([]provider.Message{ {Role: provider.RoleSystem, Content: "sys"}, {Role: provider.RoleUser, Content: filler}, @@ -642,7 +642,7 @@ func TestConcurrentLoadDuringAppendsStaysConsistent(t *testing.T) { } } }() - for i := 0; i < 40; i++ { + for i := range 40 { s.Add(provider.Message{Role: provider.RoleAssistant, Content: strings.Repeat("a", 512)}) if err := s.SaveSnapshot(path); err != nil { t.Fatalf("SaveSnapshot %d: %v", i, err) diff --git a/internal/agent/session_lease_test.go b/internal/agent/session_lease_test.go index ff97b3dee9..465c0b7047 100644 --- a/internal/agent/session_lease_test.go +++ b/internal/agent/session_lease_test.go @@ -179,14 +179,12 @@ func TestSessionLeaseConcurrentReclaimSingleWinner(t *testing.T) { leases := make(chan *SessionLease, attempts) start := make(chan struct{}) for range attempts { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { <-start if lease, err := TryReclaimCurrentProcessSessionLease(userPath); err == nil && lease != nil { leases <- lease } - }() + }) } close(start) wg.Wait() diff --git a/internal/agent/session_meta_ledger_test.go b/internal/agent/session_meta_ledger_test.go index 16936c54bf..c875f39bd3 100644 --- a/internal/agent/session_meta_ledger_test.go +++ b/internal/agent/session_meta_ledger_test.go @@ -314,9 +314,7 @@ func TestSessionMetaConcurrentWritersKeepRevisionMonotonic(t *testing.T) { metaErrCh := make(chan error, 2) var regressed atomic.Bool - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { for i := 0; ; i++ { select { case <-stop: @@ -338,10 +336,8 @@ func TestSessionMetaConcurrentWritersKeepRevisionMonotonic(t *testing.T) { return } } - }() - wg.Add(1) - go func() { - defer wg.Done() + }) + wg.Go(func() { last := int64(0) for { select { @@ -357,7 +353,7 @@ func TestSessionMetaConcurrentWritersKeepRevisionMonotonic(t *testing.T) { last = meta.Revision } } - }() + }) for i := 1; i <= saves; i++ { s.Add(provider.Message{Role: provider.RoleUser, Content: fmt.Sprintf("turn %d", i)}) @@ -445,10 +441,8 @@ func TestConcurrentSnapshotSaversNeverConflict(t *testing.T) { stop := make(chan struct{}) errCh := make(chan error, 64) var wg sync.WaitGroup - for i := 0; i < 2; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range 2 { + wg.Go(func() { for { if err := s.SaveSnapshot(path); err != nil { select { @@ -462,7 +456,7 @@ func TestConcurrentSnapshotSaversNeverConflict(t *testing.T) { default: } } - }() + }) } for i := 1; i <= adds; i++ { s.Add(provider.Message{Role: provider.RoleUser, Content: fmt.Sprintf("turn %d", i)}) diff --git a/internal/agent/storm_test.go b/internal/agent/storm_test.go index 78557476a4..fb9caeae38 100644 --- a/internal/agent/storm_test.go +++ b/internal/agent/storm_test.go @@ -59,7 +59,7 @@ func TestStormBreakerEscalatesRepeatedFailure(t *testing.T) { args := []string{`{"content":"Mountains are`, `{"path":"n.txt","content":"Peaks rise`, `{}`} var last string - for i := 0; i < stormBreakThreshold; i++ { + for i := range stormBreakThreshold { call := provider.ToolCall{Name: "write_file", Arguments: args[i]} last = executeBatchOutputs(a, context.Background(), []provider.ToolCall{call})[0] } @@ -97,7 +97,7 @@ func TestStormBreakerEscalatesRepeatedBlockedPermission(t *testing.T) { `{"command":"ls -la"}`, } var last string - for i := 0; i < stormBreakThreshold; i++ { + for i := range stormBreakThreshold { call := provider.ToolCall{Name: "bash", Arguments: args[i]} last = executeBatchOutputs(a, context.Background(), []provider.ToolCall{call})[0] } @@ -191,7 +191,7 @@ func TestStormBreakerEscalatesRepeatedBatch(t *testing.T) { {Name: "write_b", Arguments: `{"content":"y`}, } var first string - for i := 0; i < stormBreakThreshold; i++ { + for range stormBreakThreshold { first = executeBatchOutputs(a, context.Background(), batch)[0] } @@ -221,7 +221,7 @@ func TestStormBreakerBatchResetsOnPartialSuccess(t *testing.T) { {Name: "read_file", Arguments: `{"path":"x"}`}, } var first string - for i := 0; i < stormBreakThreshold+2; i++ { + for range stormBreakThreshold + 2 { first = executeBatchOutputs(a, context.Background(), batch)[0] } @@ -243,7 +243,7 @@ func TestStormBreakerSilentBelowThreshold(t *testing.T) { call := provider.ToolCall{Name: "write_file", Arguments: `{"content":"x`} var last string - for i := 0; i < stormBreakThreshold-1; i++ { + for range stormBreakThreshold - 1 { last = executeBatchOutputs(a, context.Background(), []provider.ToolCall{call})[0] } diff --git a/internal/agent/subagent_progress.go b/internal/agent/subagent_progress.go index b0696c6d37..e550228e72 100644 --- a/internal/agent/subagent_progress.go +++ b/internal/agent/subagent_progress.go @@ -2,6 +2,7 @@ package agent import ( "context" + "slices" "sync" "time" "unicode/utf8" @@ -376,7 +377,7 @@ func (m *subagentProgressMerger) stepLocked() bool { return false } now := m.clock.Now() - for i := 0; i < n; i++ { + for i := range n { idx := (m.rr + i) % n childID := m.order[idx] if m.tokens < 1 { @@ -548,10 +549,8 @@ func (m *subagentProgressMerger) pendingBytesLocked(childID string) int { } func (m *subagentProgressMerger) ensureOrderLocked(childID string) { - for _, id := range m.order { - if id == childID { - return - } + if slices.Contains(m.order, childID) { + return } m.order = append(m.order, childID) } diff --git a/internal/agent/subagent_progress_test.go b/internal/agent/subagent_progress_test.go index ca7c6255d6..fa061f5a4c 100644 --- a/internal/agent/subagent_progress_test.go +++ b/internal/agent/subagent_progress_test.go @@ -476,7 +476,7 @@ func TestSubagentProgressGroupBudgetBoundsBurstAndServesAll(t *testing.T) { t.Cleanup(merger.Close) const n = 64 - for i := 0; i < n; i++ { + for i := range n { child := "child-" + string(rune('0'+i/10)) + string(rune('0'+i%10)) // Ordinary phase transitions share the budget with previews: 64 // status changes alone must not exceed the 32 events/s contract. @@ -501,7 +501,7 @@ func TestSubagentProgressGroupBudgetBoundsBurstAndServesAll(t *testing.T) { // Once the budget refills, every child is served exactly once — no child // starves behind a high-activity sibling. var rest []event.Event - for i := 0; i < 16; i++ { + for range 16 { clock.Advance(time.Second) rest = append(rest, collectFor(t, ch, 50*time.Millisecond)...) } diff --git a/internal/agent/subagent_result.go b/internal/agent/subagent_result.go index f5585e5087..d30f931d8a 100644 --- a/internal/agent/subagent_result.go +++ b/internal/agent/subagent_result.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "slices" "strings" "unicode/utf8" @@ -91,10 +92,7 @@ func (t *SubagentResultTool) Execute(ctx context.Context, args json.RawMessage) if p.OffsetBytes < len(answer) && !utf8.RuneStart(answer[p.OffsetBytes]) { return "", fmt.Errorf("offset_bytes %d is not at a UTF-8 character boundary; use next_offset_bytes from the previous page", p.OffsetBytes) } - end := p.OffsetBytes + p.LimitBytes - if end > len(answer) { - end = len(answer) - } + end := min(p.OffsetBytes+p.LimitBytes, len(answer)) for end > p.OffsetBytes && end < len(answer) && !utf8.RuneStart(answer[end]) { end-- } @@ -154,9 +152,9 @@ func (s *SubagentStore) ReadFinalAnswer(ref, parentSession, workspaceRoot string return "", meta.Status, fmt.Errorf("load subagent transcript %q: %w", ref, err) } msgs := sess.Snapshot() - for i := len(msgs) - 1; i >= 0; i-- { - if msgs[i].Role == provider.RoleAssistant && strings.TrimSpace(msgs[i].Content) != "" { - return msgs[i].Content, meta.Status, nil + for _, v := range slices.Backward(msgs) { + if v.Role == provider.RoleAssistant && strings.TrimSpace(v.Content) != "" { + return v.Content, meta.Status, nil } } return "", meta.Status, fmt.Errorf("subagent reference %q has no final assistant answer", ref) @@ -183,10 +181,7 @@ func formatBoundedSubagentAggregate(prefix string, items []subagentAggregateItem completed++ } } - available := subagentAggregateBudgetBytes - baseBytes - if available < 0 { - available = 0 - } + available := max(subagentAggregateBudgetBytes-baseBytes, 0) perAnswer := 0 if completed > 0 { perAnswer = available / completed @@ -280,8 +275,8 @@ func splitSubagentRunResult(output string) (answer, ref string) { return strings.TrimSpace(output), "" } const marker = "\n\nFinal answer:\n" - if idx := strings.Index(output, marker); idx >= 0 { - return strings.TrimSpace(output[idx+len(marker):]), ref + if _, after, ok := strings.Cut(output, marker); ok { + return strings.TrimSpace(after), ref } return strings.TrimSpace(output), ref } diff --git a/internal/agent/task.go b/internal/agent/task.go index 62a1c16b52..9ed0e9c416 100644 --- a/internal/agent/task.go +++ b/internal/agent/task.go @@ -9,6 +9,7 @@ import ( "os" "path/filepath" "runtime/debug" + "slices" "strconv" "strings" "time" @@ -670,10 +671,7 @@ func (t *TaskTool) childMaxSteps(requested int) int { if t.maxSteps <= 0 { return 0 } - half := t.maxSteps / 2 - if half < 5 { - half = 5 - } + half := max(t.maxSteps/2, 5) return half } @@ -2045,8 +2043,8 @@ func latestAssistantAnswer(sess *Session) string { if sess == nil { return "" } - for i := len(sess.Messages) - 1; i >= 0; i-- { - m := sess.Messages[i] + for _, v := range slices.Backward(sess.Messages) { + m := v if m.Role == provider.RoleAssistant && strings.TrimSpace(m.Content) != "" { return m.Content } diff --git a/internal/agent/task_test.go b/internal/agent/task_test.go index 578e1bc78e..f3f429711a 100644 --- a/internal/agent/task_test.go +++ b/internal/agent/task_test.go @@ -825,7 +825,7 @@ func TestTaskToolBackgroundCapRefusesFanOut(t *testing.T) { // Saturate the cap with still-running task jobs owned by this session. release := make(chan struct{}) var ids []string - for i := 0; i < maxConcurrentBackgroundTasks; i++ { + for range maxConcurrentBackgroundTasks { j := jm.StartForSession("parent-session", "task", "busy", func(jctx context.Context, _ io.Writer) (string, error) { select { case <-release: @@ -1124,9 +1124,9 @@ func extractJobID(msg string) string { func subagentRefFromOutput(t *testing.T, out string) string { t.Helper() - for _, line := range strings.Split(out, "\n") { - if strings.HasPrefix(line, "Subagent reference: ") { - return strings.TrimSpace(strings.TrimPrefix(line, "Subagent reference: ")) + for line := range strings.SplitSeq(out, "\n") { + if after, ok := strings.CutPrefix(line, "Subagent reference: "); ok { + return strings.TrimSpace(after) } } t.Fatalf("no subagent reference in output:\n%s", out) diff --git a/internal/agent/textsink.go b/internal/agent/textsink.go index 6bca3fb57f..e26c5d97e3 100644 --- a/internal/agent/textsink.go +++ b/internal/agent/textsink.go @@ -166,7 +166,7 @@ func (s *TextSink) Emit(e event.Event) { break // aborted pass — the caller's Notice already explained why } fmt.Fprintln(s.out, dimText(fmt.Sprintf(" ⋯ compacted %d messages (%s)", c.Messages, c.Trigger))) - for _, ln := range strings.Split(strings.TrimRight(c.Summary, "\n"), "\n") { + for ln := range strings.SplitSeq(strings.TrimRight(c.Summary, "\n"), "\n") { fmt.Fprintln(s.out, dimText(" "+ln)) } s.wroteAnything = true diff --git a/internal/agent/todo_progress_guard_test.go b/internal/agent/todo_progress_guard_test.go index 5dea115c4e..bbdb0b64dc 100644 --- a/internal/agent/todo_progress_guard_test.go +++ b/internal/agent/todo_progress_guard_test.go @@ -21,7 +21,7 @@ func TestTodoProgressGuardPausesSemanticToolDrift(t *testing.T) { Arguments: `{"todos":[{"content":"finish the task","status":"in_progress"}]}`, }}}} // The first unique read renews the lease; exact repeats after it do not. - for i := 0; i < maxTodoStallRounds+1; i++ { + for i := range maxTodoStallRounds + 1 { turns = append(turns, testutil.Turn{ToolCalls: []provider.ToolCall{{ ID: fmt.Sprintf("read-%d", i), Name: "inspect", Arguments: `{"path":"same"}`, }}}) @@ -51,7 +51,7 @@ func TestTodoProgressGuardRenewsOnUniqueHostWork(t *testing.T) { ID: "todo", Name: "todo_write", Arguments: `{"todos":[{"content":"finish the task","status":"in_progress"}]}`, }}}} - for i := 0; i < todoProgressNudgeRounds-1; i++ { + for i := range todoProgressNudgeRounds - 1 { turns = append(turns, testutil.Turn{ToolCalls: []provider.ToolCall{{ ID: fmt.Sprintf("read-a-%d", i), Name: "inspect", Arguments: `{"path":"same"}`, }}}) @@ -59,7 +59,7 @@ func TestTodoProgressGuardRenewsOnUniqueHostWork(t *testing.T) { turns = append(turns, testutil.Turn{ToolCalls: []provider.ToolCall{{ID: "write", Name: "write_file", Arguments: `{"path":"result.txt","content":"done"}`}}}, ) - for i := 0; i < todoProgressNudgeRounds-1; i++ { + for i := range todoProgressNudgeRounds - 1 { turns = append(turns, testutil.Turn{ToolCalls: []provider.ToolCall{{ ID: fmt.Sprintf("read-b-%d", i), Name: "inspect", Arguments: `{"path":"same"}`, }}}) diff --git a/internal/agent/usecapability.go b/internal/agent/usecapability.go index fb75ef0fde..f373840823 100644 --- a/internal/agent/usecapability.go +++ b/internal/agent/usecapability.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "maps" "sort" "strings" "sync" @@ -330,9 +331,7 @@ func cloneMCPSpec(in plugin.Spec) plugin.Spec { out.Headers = cloneStringMap(in.Headers) if in.ToolTimeouts != nil { out.ToolTimeouts = make(map[string]time.Duration, len(in.ToolTimeouts)) - for name, timeout := range in.ToolTimeouts { - out.ToolTimeouts[name] = timeout - } + maps.Copy(out.ToolTimeouts, in.ToolTimeouts) } return out } @@ -342,9 +341,7 @@ func cloneStringMap(in map[string]string) map[string]string { return nil } out := make(map[string]string, len(in)) - for key, value := range in { - out[key] = value - } + maps.Copy(out, in) return out } diff --git a/internal/agent/usecapability_test.go b/internal/agent/usecapability_test.go index 3991ae42a2..623ae6c3ec 100644 --- a/internal/agent/usecapability_test.go +++ b/internal/agent/usecapability_test.go @@ -1666,7 +1666,7 @@ func TestMCPCapabilityRuntimeConcurrentUpdatesAndSnapshots(t *testing.T) { wg.Add(2) go func() { defer wg.Done() - for i := 0; i < 100; i++ { + for i := range 100 { entry.URL = fmt.Sprintf("http://127.0.0.1:%d", 10000+i) runtime.UpsertServer(entry, plugin.Spec{Name: "race", Type: "http", URL: entry.URL, Authorized: true}, true) runtime.state.setLiveTools("race", []plugin.CachedTool{{Name: "query", ReadOnly: true}}) @@ -1678,7 +1678,7 @@ func TestMCPCapabilityRuntimeConcurrentUpdatesAndSnapshots(t *testing.T) { }() go func() { defer wg.Done() - for i := 0; i < 100; i++ { + for range 100 { _, _ = frontend.Execute(context.Background(), json.RawMessage(`{"action":"list"}`)) _, _, _, _, _ = runtime.CapabilityCatalogState() } diff --git a/internal/agent/width.go b/internal/agent/width.go index 42498422f2..a5607c8d91 100644 --- a/internal/agent/width.go +++ b/internal/agent/width.go @@ -31,7 +31,7 @@ func streamedRows(s string, width int) int { width = 80 } rows := 0 - for _, line := range strings.Split(s, "\n") { + for line := range strings.SplitSeq(s, "\n") { if w := visibleWidth(line); w > 0 { rows += (w - 1) / width } diff --git a/internal/agent/write_claims.go b/internal/agent/write_claims.go index 1b333a3b55..b351abbbde 100644 --- a/internal/agent/write_claims.go +++ b/internal/agent/write_claims.go @@ -134,7 +134,7 @@ func (s WritePathSet) Overlaps(other WritePathSet) bool { // ValidateNonOverlappingWriteClaims fails if any pair of claims overlaps. // Used by fleet preflight so no task starts when path division is invalid. func ValidateNonOverlappingWriteClaims(claims []WritePathSet) error { - for i := 0; i < len(claims); i++ { + for i := range claims { if claims[i].Empty() { continue } diff --git a/internal/autoresearch/bounded_store_test.go b/internal/autoresearch/bounded_store_test.go index 5b31dad158..03f4a95cc0 100644 --- a/internal/autoresearch/bounded_store_test.go +++ b/internal/autoresearch/bounded_store_test.go @@ -157,7 +157,7 @@ func TestTailJSONLLinesMatchesFullScan(t *testing.T) { } // Write enough heartbeats that the log exceeds one 64KiB chunk. long := strings.Repeat("x", 700) - for i := 0; i < 150; i++ { + for i := range 150 { if err := store.AppendHeartbeat(task.ID, Heartbeat{ Status: HeartbeatTurnDone, Iteration: i + 1, @@ -211,7 +211,7 @@ func TestFindingsBoundedTailMatchesFullScan(t *testing.T) { if err != nil { t.Fatalf("CreateTask: %v", err) } - for i := 0; i < 25; i++ { + for i := range 25 { if err := store.AppendFinding(task.ID, Finding{ ID: fmt.Sprintf("f%03d", i), Kind: FindingKindManual, diff --git a/internal/autoresearch/store.go b/internal/autoresearch/store.go index a3b35daa6e..6016ce998c 100644 --- a/internal/autoresearch/store.go +++ b/internal/autoresearch/store.go @@ -13,6 +13,7 @@ import ( "os" "path/filepath" "regexp" + "slices" "sort" "strings" "sync" @@ -823,10 +824,7 @@ func tailJSONLLines(root *os.Root, path string, limit int) ([][]byte, error) { off = info.Size() ) for off > 0 { - readLen := int64(chunkSize) - if off < readLen { - readLen = off - } + readLen := min(off, int64(chunkSize)) off -= readLen chunk := make([]byte, readLen) if _, err := f.ReadAt(chunk, off); err != nil { @@ -923,12 +921,7 @@ func validateHeartbeat(h Heartbeat) error { } func stringSliceContains(values []string, want string) bool { - for _, value := range values { - if value == want { - return true - } - } - return false + return slices.Contains(values, want) } func cloneCriteria(in []SuccessCriterion) []SuccessCriterion { diff --git a/internal/autoresearch/store_test.go b/internal/autoresearch/store_test.go index 87f3bc51fe..d156415db2 100644 --- a/internal/autoresearch/store_test.go +++ b/internal/autoresearch/store_test.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "runtime" + "slices" "strings" "sync" "testing" @@ -124,7 +125,7 @@ func TestCreateTaskReservesIDsAtomicallyAcrossStores(t *testing.T) { start := make(chan struct{}) var ready sync.WaitGroup ready.Add(workers) - for i := 0; i < workers; i++ { + for range workers { go func() { store := NewStore(root) ready.Done() @@ -137,7 +138,7 @@ func TestCreateTaskReservesIDsAtomicallyAcrossStores(t *testing.T) { close(start) ids := map[string]string{} - for i := 0; i < workers; i++ { + for range workers { res := <-results if res.err != nil { t.Fatalf("CreateTask worker failed: %v", res.err) @@ -482,7 +483,7 @@ func TestConcurrentDirectionWritesAreSerializedPerTask(t *testing.T) { const writers = 20 var wg sync.WaitGroup errs := make(chan error, writers) - for i := 0; i < writers; i++ { + for i := range writers { wg.Add(1) go func(i int) { defer wg.Done() @@ -657,12 +658,7 @@ func containsValidationError(errors []ValidationError, file, field string) bool } func containsString(values []string, want string) bool { - for _, value := range values { - if value == want { - return true - } - } - return false + return slices.Contains(values, want) } func ptrString(s string) *string { diff --git a/internal/boot/boot.go b/internal/boot/boot.go index 3d448919d5..8b8cffd7e3 100644 --- a/internal/boot/boot.go +++ b/internal/boot/boot.go @@ -17,6 +17,7 @@ import ( "os" "path/filepath" "runtime" + "slices" "strconv" "strings" "sync/atomic" @@ -896,10 +897,7 @@ func build(ctx context.Context, opts Options) (*BuildResult, error) { cleanup = func() { prev(); lspMgr.Close() } } - maxSteps := 0 - if opts.MaxSteps > 0 { - maxSteps = opts.MaxSteps - } + maxSteps := max(opts.MaxSteps, 0) subagentStore, err := newSubagentStore(sessionDir, opts.SubagentParentLive) if err != nil { return nil, err @@ -1407,7 +1405,6 @@ func build(ctx context.Context, opts Options) (*BuildResult, error) { var slashEntries []command.SlashEntry if includeSkills { for _, sk := range skillStore.SlashList() { - sk := sk slashEntries = append(slashEntries, command.SlashEntry{ Name: sk.SlashName(), Description: sk.Description, @@ -1419,7 +1416,7 @@ func build(ctx context.Context, opts Options) (*BuildResult, error) { if cmd.Hidden { continue } - cmd := cmd + slashEntries = append(slashEntries, command.SlashEntry{ Name: cmd.Name, Description: cmd.Description, @@ -2349,13 +2346,7 @@ func SubagentModelKeys(name string) []string { if alias == "" { continue } - seen := false - for _, key := range keys { - if key == alias { - seen = true - break - } - } + seen := slices.Contains(keys, alias) if !seen { keys = append(keys, alias) } diff --git a/internal/boot/boot_test.go b/internal/boot/boot_test.go index 49e6fa57c8..b1dd389fdf 100644 --- a/internal/boot/boot_test.go +++ b/internal/boot/boot_test.go @@ -905,9 +905,9 @@ func (p *bootSubagentTestProvider) requestsSnapshot() []provider.Request { } func bootLastUser(req provider.Request) string { - for i := len(req.Messages) - 1; i >= 0; i-- { - if req.Messages[i].Role == provider.RoleUser { - return req.Messages[i].Content + for _, v := range slices.Backward(req.Messages) { + if v.Role == provider.RoleUser { + return v.Content } } return "" @@ -919,9 +919,9 @@ func subagentRefFromHistory(t *testing.T, msgs []provider.Message) string { if msg.Role != provider.RoleTool { continue } - for _, line := range strings.Split(msg.Content, "\n") { - if strings.HasPrefix(line, "Subagent reference: ") { - return strings.TrimSpace(strings.TrimPrefix(line, "Subagent reference: ")) + for line := range strings.SplitSeq(msg.Content, "\n") { + if after, ok := strings.CutPrefix(line, "Subagent reference: "); ok { + return strings.TrimSpace(after) } } } @@ -967,20 +967,20 @@ model = "x" t.Fatalf("headless run should keep an empty session path, got %q", got) } - var toolContent string + var toolContent strings.Builder for _, msg := range ctrl.History() { if msg.Role == provider.RoleTool { - toolContent += "\n" + msg.Content + toolContent.WriteString("\n" + msg.Content) } } - if strings.Contains(toolContent, "parent session is required") { - t.Fatalf("task subagent failed in headless run mode: %s", toolContent) + if strings.Contains(toolContent.String(), "parent session is required") { + t.Fatalf("task subagent failed in headless run mode: %s", toolContent.String()) } - if !strings.Contains(toolContent, "subagent answer") { - t.Fatalf("task tool result = %q, want sub-agent answer", toolContent) + if !strings.Contains(toolContent.String(), "subagent answer") { + t.Fatalf("task tool result = %q, want sub-agent answer", toolContent.String()) } - if strings.Contains(toolContent, "Subagent reference") { - t.Fatalf("ephemeral headless run should not persist a transcript reference: %s", toolContent) + if strings.Contains(toolContent.String(), "Subagent reference") { + t.Fatalf("ephemeral headless run should not persist a transcript reference: %s", toolContent.String()) } } @@ -2740,17 +2740,17 @@ READ ONLY SKILL BODY`) t.Fatalf("read_only_skill child request should hide %q; tools=%v", forbidden, toolSchemaNames(subReq.Tools)) } } - var toolOutput string + var toolOutput strings.Builder for _, msg := range ctrl.History() { if msg.Role == provider.RoleTool && msg.Name == "connect_tool_source" { - toolOutput += msg.Content + toolOutput.WriteString(msg.Content) if strings.Contains(msg.Content, "blocked:") { t.Fatalf("connect_tool_source should not block read_only_skill in plan mode, got:\n%s", msg.Content) } } } - if !strings.Contains(toolOutput, "readonlydig") || !strings.Contains(toolOutput, "# Skills") { - t.Fatalf("read_only_skill source result should include the skill index, got:\n%s", toolOutput) + if !strings.Contains(toolOutput.String(), "readonlydig") || !strings.Contains(toolOutput.String(), "# Skills") { + t.Fatalf("read_only_skill source result should include the skill index, got:\n%s", toolOutput.String()) } } @@ -2954,17 +2954,17 @@ command = "reasonix-missing-mockmcp" if len(reqs) != 2 { t.Fatalf("requests = %d, want 2", len(reqs)) } - var toolOutput string + var toolOutput strings.Builder for _, msg := range ctrl.History() { if msg.Role == provider.RoleTool && msg.Name == "connect_tool_source" { - toolOutput += msg.Content + toolOutput.WriteString(msg.Content) } } - if strings.TrimSpace(toolOutput) == "" { + if strings.TrimSpace(toolOutput.String()) == "" { t.Fatalf("connect_tool_source(%s) returned empty tool output", tt.source) } - if strings.Contains(toolOutput, "blocked:") { - t.Fatalf("connect_tool_source(%s) should load capability metadata in Plan, got %q", tt.source, toolOutput) + if strings.Contains(toolOutput.String(), "blocked:") { + t.Fatalf("connect_tool_source(%s) should load capability metadata in Plan, got %q", tt.source, toolOutput.String()) } for _, enabled := range tt.enabledTools { if !requestHasTool(reqs[1], enabled) { @@ -3024,17 +3024,17 @@ model = "x" if requestHasTool(reqs[1], "complete_step") { t.Fatalf("plan-mode workflow connect must not expose complete_step; tools=%v", toolSchemaNames(reqs[1].Tools)) } - var planConnectOutput string + var planConnectOutput strings.Builder for _, msg := range ctrl.History() { if msg.Role == provider.RoleTool && msg.Name == "connect_tool_source" { - planConnectOutput += msg.Content + planConnectOutput.WriteString(msg.Content) } } - if strings.Contains(planConnectOutput, "blocked:") { - t.Fatalf("workflow source should not be blocked in plan mode, got:\n%s", planConnectOutput) + if strings.Contains(planConnectOutput.String(), "blocked:") { + t.Fatalf("workflow source should not be blocked in plan mode, got:\n%s", planConnectOutput.String()) } - if !strings.Contains(planConnectOutput, "complete_step stays blocked in plan mode") { - t.Fatalf("plan-mode workflow connect should explain the deferred complete_step, got:\n%s", planConnectOutput) + if !strings.Contains(planConnectOutput.String(), "complete_step stays blocked in plan mode") { + t.Fatalf("plan-mode workflow connect should explain the deferred complete_step, got:\n%s", planConnectOutput.String()) } ctrl.SetPlanMode(false) @@ -3137,14 +3137,14 @@ model = "x" if requestHasTool(reqs[1], "web_fetch") { t.Fatalf("disabled web_fetch should not be exposed after connect_tool_source; tools=%v", toolSchemaNames(reqs[1].Tools)) } - var toolOutput string + var toolOutput strings.Builder for _, msg := range ctrl.History() { if msg.Role == provider.RoleTool && msg.Name == "connect_tool_source" { - toolOutput += msg.Content + toolOutput.WriteString(msg.Content) } } - if !strings.Contains(toolOutput, "web_fetch is disabled by [tools].enabled") { - t.Fatalf("connector should explain disabled web_fetch, got:\n%s", toolOutput) + if !strings.Contains(toolOutput.String(), "web_fetch is disabled by [tools].enabled") { + t.Fatalf("connector should explain disabled web_fetch, got:\n%s", toolOutput.String()) } } @@ -3194,14 +3194,14 @@ model = "x" t.Fatalf("second request should expose %q after connect_tool_source; tools=%v", name, toolSchemaNames(reqs[1].Tools)) } } - var toolOutput string + var toolOutput strings.Builder for _, msg := range ctrl.History() { if msg.Role == provider.RoleTool && msg.Name == "connect_tool_source" { - toolOutput += msg.Content + toolOutput.WriteString(msg.Content) } } - if !strings.Contains(toolOutput, "projskill") || !strings.Contains(toolOutput, "# Skills") { - t.Fatalf("skills source result should include the skill index, got:\n%s", toolOutput) + if !strings.Contains(toolOutput.String(), "projskill") || !strings.Contains(toolOutput.String(), "# Skills") { + t.Fatalf("skills source result should include the skill index, got:\n%s", toolOutput.String()) } } @@ -3357,8 +3357,8 @@ api_key_env = "REASONIX_TEST_KEY_UNSET" // user-global REASONIX.md in the real config dir could append; the test // environment has none, so the base stands alone.) base := sys - if i := strings.Index(sys, "\n\n# Skills"); i >= 0 { - base = sys[:i] + if before, _, ok := strings.Cut(sys, "\n\n# Skills"); ok { + base = before } // The language policy, user-decision policy, and current-workspace line are // always appended at boot; strip them so this assertion is purely about @@ -3521,8 +3521,8 @@ func stripLanguagePolicy(s string) string { } func stripEnvironmentBlock(s string) string { - if i := strings.Index(s, "\n\n## Environment"); i >= 0 { - return s[:i] + if before, _, ok := strings.Cut(s, "\n\n## Environment"); ok { + return before } return s } @@ -3695,7 +3695,7 @@ func TestRememberPermissionRuleSerializesConcurrentWriters(t *testing.T) { start := make(chan struct{}) results := make(chan control.RememberResult, writers) var wg sync.WaitGroup - for i := 0; i < writers; i++ { + for i := range writers { wg.Add(1) go func(n int) { defer wg.Done() @@ -3713,7 +3713,7 @@ func TestRememberPermissionRuleSerializesConcurrentWriters(t *testing.T) { } got := config.LoadForEdit(filepath.Join(workspace, "reasonix.toml")) - for i := 0; i < writers; i++ { + for i := range writers { rule := fmt.Sprintf("Edit(file-%02d)", i) if !hasPermissionRule(got.Permissions.Allow, rule) { t.Errorf("permissions.allow missing %q: %v", rule, got.Permissions.Allow) @@ -3731,7 +3731,7 @@ func TestRememberPermissionRuleSerializesCrossProcessWriters(t *testing.T) { const rulesPerWorker = 8 commands := make([]*exec.Cmd, 0, workers) outputs := make([]bytes.Buffer, workers) - for worker := 0; worker < workers; worker++ { + for worker := range workers { cmd := exec.Command(os.Args[0], "-test.run=^TestRememberPermissionRuleProcessHelper$") cmd.Stdout = &outputs[worker] cmd.Stderr = &outputs[worker] @@ -3778,8 +3778,8 @@ func TestRememberPermissionRuleSerializesCrossProcessWriters(t *testing.T) { } got := config.LoadForEdit(filepath.Join(workspace, "reasonix.toml")) - for worker := 0; worker < workers; worker++ { - for n := 0; n < rulesPerWorker; n++ { + for worker := range workers { + for n := range rulesPerWorker { rule := fmt.Sprintf("Edit(process-%d-file-%02d)", worker, n) if !hasPermissionRule(got.Permissions.Allow, rule) { t.Errorf("permissions.allow missing %q: %v", rule, got.Permissions.Allow) @@ -3817,7 +3817,7 @@ func TestRememberPermissionRuleProcessHelper(t *testing.T) { } time.Sleep(10 * time.Millisecond) } - for n := 0; n < rules; n++ { + for n := range rules { rule := fmt.Sprintf("Edit(process-%d-file-%02d)", worker, n) result := rememberPermissionRule(workspace, rule) if result.Err != nil || !result.Saved { @@ -4008,12 +4008,7 @@ plan_mode_read_only_commands = ["gh issue view"] } func hasPermissionRule(rules []string, want string) bool { - for _, rule := range rules { - if rule == want { - return true - } - } - return false + return slices.Contains(rules, want) } func hasPlanModeReadOnlyCommand(commands []string, want string) bool { @@ -4907,7 +4902,7 @@ func TestBuildMigratesLegacyEagerBeforeStatsDemotion(t *testing.T) { // Three samples above 2*budget — the rule in stats.go's Recommend triggers // when the trailing window is entirely over the threshold. Use 30s so even // future budget bumps stay below the threshold. - for i := 0; i < 3; i++ { + for i := range 3 { if err := plugin.RecordStartup("slowserver", 30*time.Second); err != nil { t.Fatalf("RecordStartup #%d: %v", i, err) } @@ -4990,8 +4985,7 @@ func TestBuildExtraPluginProbeKeepsSessionProcessAlive(t *testing.T) { workspace := robustTempDir(t) t.Chdir(workspace) - sessionCtx, cancelSession := context.WithCancel(context.Background()) - defer cancelSession() + sessionCtx := t.Context() ctrl, err := Build(sessionCtx, Options{ SessionDir: filepath.Join(t.TempDir(), "sessions"), Sink: event.Discard, diff --git a/internal/boot/extension_dispatch_test.go b/internal/boot/extension_dispatch_test.go index 8185fa4fcd..324b73ecd5 100644 --- a/internal/boot/extension_dispatch_test.go +++ b/internal/boot/extension_dispatch_test.go @@ -260,7 +260,7 @@ func TestBootSystemPromptBuildEventObserved(t *testing.T) { if err != nil { return false } - for _, candidate := range strings.Split(strings.TrimSpace(string(data)), "\n") { + for candidate := range strings.SplitSeq(strings.TrimSpace(string(data)), "\n") { if strings.HasPrefix(candidate, "system_prompt.build ") { line = candidate return true diff --git a/internal/boot/extension_firstboot_test.go b/internal/boot/extension_firstboot_test.go index 759df3a2fc..aecc1353fb 100644 --- a/internal/boot/extension_firstboot_test.go +++ b/internal/boot/extension_firstboot_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "maps" "os" "path/filepath" "strconv" @@ -54,9 +55,7 @@ func installProviderFake(t *testing.T, home, name string, extraEnv map[string]st bootFakeEnvPluginName: name, bootFakeEnvProvider: "1", } - for k, v := range extraEnv { - env[k] = v - } + maps.Copy(env, extraEnv) installBootFakePlugin(t, home, name, map[string]any{ "capabilities": []string{"providers"}, "env": env, diff --git a/internal/boot/extension_provider_test.go b/internal/boot/extension_provider_test.go index 0f4d754ee5..33ff07db82 100644 --- a/internal/boot/extension_provider_test.go +++ b/internal/boot/extension_provider_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "maps" "os" "strings" "testing" @@ -32,9 +33,7 @@ func bootWithProviderPlugin(t *testing.T, name string, runtime map[string]any) * bootFakeEnvProvider: "1", } if extra, ok := runtime["env"].(map[string]string); ok { - for k, v := range extra { - env[k] = v - } + maps.Copy(env, extra) } runtime["env"] = env return bootWithFakePlugin(t, name, runtime) diff --git a/internal/boot/extension_sidecar_test.go b/internal/boot/extension_sidecar_test.go index e98deae4c1..9eeca1801b 100644 --- a/internal/boot/extension_sidecar_test.go +++ b/internal/boot/extension_sidecar_test.go @@ -82,7 +82,7 @@ func TestExtensionFakeSidecarHelperProcess(t *testing.T) { func runBootFakeSidecar(stdin io.Reader, stdout io.Writer) { if pidFile := strings.TrimSpace(os.Getenv(bootFakeEnvPIDFile)); pidFile != "" { - _ = os.WriteFile(pidFile, []byte(fmt.Sprintf("%d", os.Getpid())), 0o644) + _ = os.WriteFile(pidFile, fmt.Appendf(nil, "%d", os.Getpid()), 0o644) } if os.Getenv(bootFakeEnvExitImmediately) == "1" { os.Exit(0) diff --git a/internal/boot/prompt_stability_test.go b/internal/boot/prompt_stability_test.go index 9148243f0f..014bdab6a0 100644 --- a/internal/boot/prompt_stability_test.go +++ b/internal/boot/prompt_stability_test.go @@ -61,25 +61,13 @@ api_key_env = "REASONIX_TEST_KEY_UNSET" // firstDivergence returns a small window around the first differing byte so a // failure names the drifting prompt section instead of dumping both prompts. func firstDivergence(a, b string) string { - limit := len(a) - if len(b) < limit { - limit = len(b) - } + limit := min(len(b), len(a)) i := 0 for i < limit && a[i] == b[i] { i++ } - start := i - 40 - if start < 0 { - start = 0 - } - endA := i + 40 - if endA > len(a) { - endA = len(a) - } - endB := i + 40 - if endB > len(b) { - endB = len(b) - } + start := max(i-40, 0) + endA := min(i+40, len(a)) + endB := min(i+40, len(b)) return "..." + a[start:endA] + "... vs ..." + b[start:endB] + "..." } diff --git a/internal/boot/resolver.go b/internal/boot/resolver.go index 385bf4065a..d0ccf3688e 100644 --- a/internal/boot/resolver.go +++ b/internal/boot/resolver.go @@ -222,8 +222,8 @@ func syntheticEntryFromResolver(r provider.Resolver, ref string) *config.Provide func splitProviderRef(ref string) (string, string) { ref = strings.TrimSpace(ref) - if i := strings.IndexByte(ref, '/'); i >= 0 { - return ref[:i], ref[i+1:] + if before, after, ok := strings.Cut(ref, "/"); ok { + return before, after } return ref, "" } diff --git a/internal/boot/temphelper_test.go b/internal/boot/temphelper_test.go index a1918872d8..d09a01eec9 100644 --- a/internal/boot/temphelper_test.go +++ b/internal/boot/temphelper_test.go @@ -25,7 +25,7 @@ func robustTempDir(t *testing.T) string { } t.Cleanup(func() { var rmErr error - for i := 0; i < 100; i++ { + for range 100 { if rmErr = os.RemoveAll(dir); rmErr == nil { return } diff --git a/internal/bot/feishu/feishu.go b/internal/bot/feishu/feishu.go index fa81a927c6..02e7cb3ca8 100644 --- a/internal/bot/feishu/feishu.go +++ b/internal/bot/feishu/feishu.go @@ -17,6 +17,7 @@ import ( "fmt" "io" "log/slog" + "maps" "net/http" "os" "strings" @@ -888,24 +889,22 @@ func (a *adapter) AddPendingReaction(ctx context.Context, messageID string) (fun func (a *adapter) sendCard(ctx context.Context, msg bot.OutboundMessage) (bot.SendResult, error) { card := msg.Card - elements := make([]map[string]interface{}, 0) + elements := make([]map[string]any, 0) for _, el := range card.Elements { - item := map[string]interface{}{"tag": el.Tag} + item := map[string]any{"tag": el.Tag} if el.Content != "" { item["content"] = el.Content } if actions, ok := el.Extra["actions"]; ok && el.Tag == "action" { item["actions"] = actions } else { - for k, v := range el.Extra { - item[k] = v - } + maps.Copy(item, el.Extra) } elements = append(elements, item) } - cardPayload := map[string]interface{}{ - "header": map[string]interface{}{ + cardPayload := map[string]any{ + "header": map[string]any{ "title": map[string]string{ "tag": "plain_text", "content": card.Header, @@ -954,7 +953,7 @@ func (a *adapter) runWebhook(ctx context.Context) { mux.HandleFunc("/feishu/event", func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(io.LimitReader(r.Body, 1024*1024)) if err != nil { - http.Error(w, "bad request", 400) + http.Error(w, "bad request", http.StatusBadRequest) return } var challenge struct { @@ -977,7 +976,7 @@ func (a *adapter) runWebhook(ctx context.Context) { var evt feishuEvent if err := json.Unmarshal(body, &evt); err != nil { - http.Error(w, "bad request", 400) + http.Error(w, "bad request", http.StatusBadRequest) return } if !a.verificationTokenValid(evt.Header.Token) { @@ -989,7 +988,7 @@ func (a *adapter) runWebhook(ctx context.Context) { raw, _ := json.Marshal(evt) a.handleWSEvent(ctx, raw) } - w.WriteHeader(200) + w.WriteHeader(http.StatusOK) }) server := &http.Server{ diff --git a/internal/bot/feishu/feishu_test.go b/internal/bot/feishu/feishu_test.go index f3089b194e..6508a264ad 100644 --- a/internal/bot/feishu/feishu_test.go +++ b/internal/bot/feishu/feishu_test.go @@ -53,7 +53,7 @@ func TestMarkSeenConcurrent(t *testing.T) { a := &adapter{seen: make(map[string]bool)} var wg sync.WaitGroup - for i := 0; i < 100; i++ { + for i := range 100 { wg.Add(1) go func(i int) { defer wg.Done() diff --git a/internal/bot/gateway.go b/internal/bot/gateway.go index 05a61ecd08..5188c7b059 100644 --- a/internal/bot/gateway.go +++ b/internal/bot/gateway.go @@ -442,11 +442,9 @@ func (gw *BotGateway) Start(ctx context.Context) (err error) { // 合并所有适配器的消息通道 for _, binding := range gw.adapters { - gw.gatewayWG.Add(1) - go func() { - defer gw.gatewayWG.Done() + gw.gatewayWG.Go(func() { gw.dispatchLoop(runCtx, binding) - }() + }) } return nil @@ -836,11 +834,9 @@ func (gw *BotGateway) handleMessage(ctx context.Context, binding AdapterBinding, // would unblock it: the session wedges until restart (#4701, #4863, #4402). // Per-session serialization is still held by the session lock (active[key]), // which the deferred Release inside runTurn clears. - gw.turnWG.Add(1) - go func() { - defer gw.turnWG.Done() + gw.turnWG.Go(func() { gw.runTurn(ctx, binding.Adapter, key, msg, cleanup) - }() + }) } func (gw *BotGateway) queueMode(key string, msg InboundMessage) string { @@ -2801,7 +2797,7 @@ func parseAskAnswers(questions []event.AskQuestion, raw string) []event.AskAnswe } answerMap := make(map[string][]string, len(questions)) if strings.Contains(raw, "=") { - for _, part := range strings.Split(raw, ";") { + for part := range strings.SplitSeq(raw, ";") { k, v, ok := strings.Cut(part, "=") if !ok { continue diff --git a/internal/bot/gateway_lock_test.go b/internal/bot/gateway_lock_test.go index 32fdc3d6eb..10d8f92765 100644 --- a/internal/bot/gateway_lock_test.go +++ b/internal/bot/gateway_lock_test.go @@ -384,7 +384,7 @@ func TestBotGatewayToolApprovalModeConcurrentWithConfigReaders(t *testing.T) { defer close(writerDone) <-start modes := []string{control.ToolApprovalYolo, control.ToolApprovalAsk, control.ToolApprovalAuto} - for i := 0; i < iterations; i++ { + for i := range iterations { mode := modes[i%len(modes)] gw.UpdateConnectionToolApprovalMode("feishu-lark", mode) gw.mu.Lock() @@ -397,7 +397,7 @@ func TestBotGatewayToolApprovalModeConcurrentWithConfigReaders(t *testing.T) { close(start) connMsg := InboundMessage{Platform: PlatformFeishu, ConnectionID: "feishu-lark", ChatType: ChatDM, ChatID: "chat", UserID: "user"} - for i := 0; i < iterations; i++ { + for range iterations { gw.sessionOptionsForMessage(connMsg) gw.sessionOptionsForMessage(InboundMessage{Platform: PlatformFeishu}) projects := gw.buildProjectIndex() diff --git a/internal/bot/gateway_test.go b/internal/bot/gateway_test.go index d2c28daccc..2e991a2f6f 100644 --- a/internal/bot/gateway_test.go +++ b/internal/bot/gateway_test.go @@ -1353,8 +1353,7 @@ func TestGatewayApprovalReplyUnblocksWedgedTurn(t *testing.T) { pendingAsks: make(map[string][]event.AskQuestion), } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := t.Context() go gw.dispatchLoop(ctx, binding) adapter.msgCh <- msg @@ -1408,8 +1407,7 @@ func TestGatewayAskReplyUnblocksWedgedTurn(t *testing.T) { pendingAsks: make(map[string][]event.AskQuestion), } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := t.Context() go gw.dispatchLoop(ctx, binding) adapter.msgCh <- msg diff --git a/internal/bot/pairing.go b/internal/bot/pairing.go index 3544ede231..c5a481edf4 100644 --- a/internal/bot/pairing.go +++ b/internal/bot/pairing.go @@ -343,7 +343,7 @@ func pairingRequestMatches(req PairingRequest, msg InboundMessage) bool { func newPairingCode() (string, error) { var b strings.Builder max := big.NewInt(int64(len(pairingAlphabet))) - for i := 0; i < 8; i++ { + for range 8 { n, err := rand.Int(rand.Reader, max) if err != nil { return "", err diff --git a/internal/bot/pairing_test.go b/internal/bot/pairing_test.go index 036ced07e2..7fd92a1648 100644 --- a/internal/bot/pairing_test.go +++ b/internal/bot/pairing_test.go @@ -17,7 +17,7 @@ func TestCreateOrRefreshPairingRequestConcurrent(t *testing.T) { const workers = 8 var wg sync.WaitGroup errs := make(chan error, workers) - for i := 0; i < workers; i++ { + for i := range workers { wg.Add(1) go func(i int) { defer wg.Done() @@ -27,7 +27,7 @@ func TestCreateOrRefreshPairingRequestConcurrent(t *testing.T) { ChatID: fmt.Sprintf("chat-%d", i), UserID: fmt.Sprintf("user-%d", i), } - for j := 0; j < 5; j++ { + for range 5 { if _, _, err := CreateOrRefreshPairingRequest(msg, cfg); err != nil { errs <- err return diff --git a/internal/bot/project_index.go b/internal/bot/project_index.go index 0498034b4f..04ce390c93 100644 --- a/internal/bot/project_index.go +++ b/internal/bot/project_index.go @@ -12,6 +12,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "sort" "strings" "time" @@ -143,10 +144,8 @@ func (c *botProjectCollector) add(root, source string) { if source == "" { return } - for _, existing := range entry.Sources { - if existing == source { - return - } + if slices.Contains(entry.Sources, source) { + return } entry.Sources = append(entry.Sources, source) } @@ -367,8 +366,8 @@ func botSessionPathFromTarget(target string) string { if target == "" { return "" } - if strings.HasPrefix(target, "path:") { - return canonicalBotPath(strings.TrimPrefix(target, "path:")) + if after, ok := strings.CutPrefix(target, "path:"); ok { + return canonicalBotPath(after) } if filepath.IsAbs(target) && strings.HasSuffix(target, ".jsonl") { return canonicalBotPath(target) @@ -499,7 +498,7 @@ func formatBotProjects(projects []botProjectEntry, query string, limit int) stri } var b strings.Builder fmt.Fprintf(&b, "项目索引(%d/%d):", limit, len(matches)) - for i := 0; i < limit; i++ { + for i := range limit { project := matches[i] fmt.Fprintf(&b, "\n%s %s — %s", project.ID, project.Name, displayBotPath(project.Root)) if len(project.Sources) > 0 { @@ -525,7 +524,7 @@ func formatBotSessions(sessions []botSessionEntry, query string, limit int) stri } var b strings.Builder fmt.Fprintf(&b, "会话索引(%d/%d):", limit, len(matches)) - for i := 0; i < limit; i++ { + for i := range limit { session := matches[i] project := firstNonEmptyString(session.ProjectName, "global") fmt.Fprintf(&b, "\n%s %s", session.ID, project) @@ -559,7 +558,7 @@ func formatBotProjectSearchResults(results []botProjectSearchResult, limit int) } var b strings.Builder fmt.Fprintf(&b, "跨项目检索结果(%d/%d):", limit, len(results)) - for i := 0; i < limit; i++ { + for i := range limit { result := results[i] project := firstNonEmptyString(result.ProjectName, result.ProjectID) fmt.Fprintf(&b, "\n- %s %s:%d: %s", project, displayBotPath(result.Path), result.Line, singleLineBotText(result.Text, 120)) diff --git a/internal/bot/qq/adapter.go b/internal/bot/qq/adapter.go index b46cd7d788..4993ef3ca8 100644 --- a/internal/bot/qq/adapter.go +++ b/internal/bot/qq/adapter.go @@ -64,11 +64,9 @@ func (a *adapter) Start(ctx context.Context) error { } ctx, a.cancel = context.WithCancel(ctx) - a.loopWG.Add(1) - go func() { - defer a.loopWG.Done() + a.loopWG.Go(func() { a.gatewayLoop(ctx) - }() + }) return nil } diff --git a/internal/bot/qq/adapter_stop_test.go b/internal/bot/qq/adapter_stop_test.go index 48b08391de..f097af46af 100644 --- a/internal/bot/qq/adapter_stop_test.go +++ b/internal/bot/qq/adapter_stop_test.go @@ -33,9 +33,7 @@ func TestStopClosesTrackedConnAndWaitsForLoop(t *testing.T) { a.cancel = cancel tracked := make(chan struct{}) decodeReturned := make(chan struct{}) - a.loopWG.Add(1) - go func() { - defer a.loopWG.Done() + a.loopWG.Go(func() { if !a.trackConn(ctx, conn) { conn.Close() return @@ -45,7 +43,7 @@ func TestStopClosesTrackedConnAndWaitsForLoop(t *testing.T) { var payload gatewayPayload _ = json.NewDecoder(conn).Decode(&payload) // blocks like connectGateway's reads close(decodeReturned) - }() + }) select { case <-tracked: case <-time.After(time.Second): @@ -101,15 +99,13 @@ func TestStopUnblocksStalledHandshakeDial(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) a.cancel = cancel dialErr := make(chan error, 1) - a.loopWG.Add(1) - go func() { - defer a.loopWG.Done() + a.loopWG.Go(func() { conn, err := a.dialGateway(ctx, "ws://"+ln.Addr().String(), "test-token") if err == nil { conn.Close() } dialErr <- err - }() + }) var srvConn net.Conn select { diff --git a/internal/bot/qq/gateway.go b/internal/bot/qq/gateway.go index afb5f2ca99..ec90cb9f67 100644 --- a/internal/bot/qq/gateway.go +++ b/internal/bot/qq/gateway.go @@ -146,7 +146,7 @@ func (a *adapter) getAccessToken(ctx context.Context) (string, error) { return "", err } - req, err := http.NewRequestWithContext(ctx, "POST", qqTokenURL, bytes.NewReader(body)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, qqTokenURL, bytes.NewReader(body)) if err != nil { return "", err } @@ -421,7 +421,7 @@ func (a *adapter) apiBaseURL() string { } func (a *adapter) getGatewayURL(ctx context.Context, token string) (string, error) { - req, err := http.NewRequestWithContext(ctx, "GET", a.apiBaseURL()+"/gateway", nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.apiBaseURL()+"/gateway", nil) if err != nil { return "", err } @@ -616,7 +616,7 @@ func (a *adapter) sendMessageChunk(ctx context.Context, msg bot.OutboundMessage, } rows = append(rows, map[string]any{"buttons": buttons}) } - payload["keyboard"] = map[string]interface{}{ + payload["keyboard"] = map[string]any{ "content": rows, } return a.sendMessagePayload(ctx, msg, payload, seq) @@ -643,7 +643,7 @@ func (a *adapter) sendMessagePayload(ctx context.Context, msg bot.OutboundMessag url := a.qqSendURL(msg) body, _ := json.Marshal(payload) - req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(body)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body)) if err != nil { return bot.SendResult{}, err } diff --git a/internal/bot/render_test.go b/internal/bot/render_test.go index fb93805b76..8e28d037d4 100644 --- a/internal/bot/render_test.go +++ b/internal/bot/render_test.go @@ -253,7 +253,7 @@ func TestRenderSinkLimitsProgressMessages(t *testing.T) { adapter := newFakeAdapter(PlatformWeixin, "fake-weixin") sink := newRenderSink(context.Background(), adapter, "weixin-weixin", "weixin", "chat-1", ChatDM, "user-1", "msg-1", slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil) - for i := 0; i < renderMaxProgressMessages+2; i++ { + for range renderMaxProgressMessages + 2 { sink.lastProgress = time.Now().Add(-renderProgressMinInterval) sink.Emit(event.Event{Kind: event.ToolDispatch, Tool: event.Tool{ID: "tool", Name: "bash"}}) } diff --git a/internal/bot/session.go b/internal/bot/session.go index 8f7dcd9efa..ed8dbe14e7 100644 --- a/internal/bot/session.go +++ b/internal/bot/session.go @@ -319,11 +319,8 @@ func (sm *SessionManager) consumeDroppedPrefixLocked(key, text string) string { fmt.Fprintf(&b, "[Queue note: %d older pending message(s) were dropped because this bot session reached its queue cap.", len(dropped)) if len(dropped) > 0 { b.WriteString(" Dropped summaries:") - limit := len(dropped) - if limit > 3 { - limit = 3 - } - for i := 0; i < limit; i++ { + limit := min(len(dropped), 3) + for i := range limit { fmt.Fprintf(&b, "\n- %s", dropped[i]) } if len(dropped) > limit { diff --git a/internal/bot/turn_dispatch_test.go b/internal/bot/turn_dispatch_test.go index 5c583cc164..bc8da7882a 100644 --- a/internal/bot/turn_dispatch_test.go +++ b/internal/bot/turn_dispatch_test.go @@ -89,8 +89,7 @@ func TestGatewayApprovalReplyUnblocksTurnOffDispatchGoroutine(t *testing.T) { // building a real one via boot.Build. gw.controllers[key] = &sessionState{ctrl: ctrl, sink: &sessionEventSink{}} - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := t.Context() go gw.dispatchLoop(ctx, binding) // First message: a normal turn that will block on approval inside RunTurn. diff --git a/internal/bot/types.go b/internal/bot/types.go index adaeee4c7b..f5b45d1df2 100644 --- a/internal/bot/types.go +++ b/internal/bot/types.go @@ -4,6 +4,7 @@ package bot import ( "context" + "slices" "strings" ) @@ -144,10 +145,8 @@ func (r SendResult) DeliveredMessageIDs() []string { if id == "" { return } - for _, existing := range ids { - if existing == id { - return - } + if slices.Contains(ids, id) { + return } ids = append(ids, id) } @@ -162,13 +161,7 @@ func (r SendResult) DeliveredMessageIDs() []string { // last delivered ID for callers using the legacy singular field. func (r *SendResult) Merge(delivered SendResult) { for _, id := range delivered.DeliveredMessageIDs() { - duplicate := false - for _, existing := range r.MessageIDs { - if existing == id { - duplicate = true - break - } - } + duplicate := slices.Contains(r.MessageIDs, id) if !duplicate { r.MessageIDs = append(r.MessageIDs, id) } diff --git a/internal/bot/weixin/weixin.go b/internal/bot/weixin/weixin.go index 31e543084f..f045ca212f 100644 --- a/internal/bot/weixin/weixin.go +++ b/internal/bot/weixin/weixin.go @@ -289,7 +289,7 @@ func (a *adapter) saveContextTokens() { } func ilinkGET(ctx context.Context, baseURL, endpoint string) (map[string]any, error) { - req, err := http.NewRequestWithContext(ctx, "GET", strings.TrimRight(baseURL, "/")+"/"+strings.TrimLeft(endpoint, "/"), nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(baseURL, "/")+"/"+strings.TrimLeft(endpoint, "/"), nil) if err != nil { return nil, err } @@ -358,7 +358,7 @@ func (a *adapter) getUpdates(ctx context.Context) ([]ilinkUpdate, error) { url := a.apiBase() + getUpdatesPath a.mu.Lock() - payload := map[string]interface{}{ + payload := map[string]any{ "get_updates_buf": a.syncBuf, "base_info": map[string]string{ "channel_version": ilinkChannelVersion, @@ -367,7 +367,7 @@ func (a *adapter) getUpdates(ctx context.Context) ([]ilinkUpdate, error) { a.mu.Unlock() body, _ := json.Marshal(payload) - req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(body)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body)) if err != nil { return nil, err } @@ -542,9 +542,9 @@ func setIlinkHeaders(req *http.Request, token string, body []byte) { func randomWechatUIN() string { var b [4]byte if _, err := rand.Read(b[:]); err != nil { - return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%d", time.Now().UnixNano()))) + return base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%d", time.Now().UnixNano())) } - return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%d", uint32(b[0])<<24|uint32(b[1])<<16|uint32(b[2])<<8|uint32(b[3])))) + return base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%d", uint32(b[0])<<24|uint32(b[1])<<16|uint32(b[2])<<8|uint32(b[3]))) } func firstNonEmptyString(vals ...string) string { @@ -565,27 +565,27 @@ func (a *adapter) sendMessage(ctx context.Context, msg bot.OutboundMessage) (bot url := a.apiBase() + sendMessagePath - payload := map[string]interface{}{ + payload := map[string]any{ "base_info": map[string]string{"channel_version": ilinkChannelVersion}, - "msg": map[string]interface{}{ + "msg": map[string]any{ "from_user_id": "", "to_user_id": msg.ChatID, "client_id": fmt.Sprintf("reasonix-%d", time.Now().UnixNano()), "message_type": weixinMsgTypeBot, "message_state": weixinMsgStateDone, - "item_list": []map[string]interface{}{ + "item_list": []map[string]any{ {"type": weixinItemText, "text_item": map[string]string{"text": msg.Text}}, }, }, } if contextToken := a.contextToken(msg.ChatID); contextToken != "" { - if m, ok := payload["msg"].(map[string]interface{}); ok { + if m, ok := payload["msg"].(map[string]any); ok { m["context_token"] = contextToken } } body, _ := json.Marshal(payload) - req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(body)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body)) if err != nil { return bot.SendResult{}, err } @@ -627,7 +627,7 @@ func (a *adapter) sendTyping(ctx context.Context, chatID string) error { url := a.apiBase() + sendTypingPath - payload := map[string]interface{}{ + payload := map[string]any{ "base_info": map[string]string{"channel_version": ilinkChannelVersion}, "ilink_user_id": chatID, "status": 1, @@ -637,7 +637,7 @@ func (a *adapter) sendTyping(ctx context.Context, chatID string) error { } body, _ := json.Marshal(payload) - req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(body)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body)) if err != nil { return err } diff --git a/internal/capability/capability.go b/internal/capability/capability.go index 23724176e4..c1bf98329c 100644 --- a/internal/capability/capability.go +++ b/internal/capability/capability.go @@ -218,10 +218,7 @@ func limitRouteCandidates(candidates []RouteCandidate) []RouteCandidate { suggested = append(suggested, candidate) } } - slots := targetCandidates - len(strong) - if slots < 0 { - slots = 0 - } + slots := max(targetCandidates-len(strong), 0) if len(suggested) > slots { suggested = suggested[:slots] } diff --git a/internal/capability/capability_test.go b/internal/capability/capability_test.go index 98e01678ca..357d5e0da6 100644 --- a/internal/capability/capability_test.go +++ b/internal/capability/capability_test.go @@ -71,7 +71,7 @@ func TestRouteRespectsSkillAutoUseMetadata(t *testing.T) { func TestRouteKeepsAllStrongCandidatesBeforeSuggestBudget(t *testing.T) { entries := make([]Entry, 0, 8) - for i := 0; i < 6; i++ { + for i := range 6 { entries = append(entries, Entry{ID: fmt.Sprintf("skill:required-%d", i), Kind: KindSkill, Name: fmt.Sprintf("required-%d", i), AutoUse: AutoUsePrefer, Triggers: []string{"ship"}}) } entries = append(entries, diff --git a/internal/capability/semantic.go b/internal/capability/semantic.go index 3cd051b028..07ef888e67 100644 --- a/internal/capability/semantic.go +++ b/internal/capability/semantic.go @@ -106,7 +106,7 @@ func semanticPool(text string, entries []Entry) []Entry { // bounded built-in/high-policy Skill set so English metadata does not make // the semantic router blind to Chinese requests. matched := false - for _, tok := range strings.Fields(text) { + for tok := range strings.FieldsSeq(text) { if len(tok) < 3 { continue } diff --git a/internal/checkpoint/checkpoint.go b/internal/checkpoint/checkpoint.go index 73cab16894..ca0909ca0f 100644 --- a/internal/checkpoint/checkpoint.go +++ b/internal/checkpoint/checkpoint.go @@ -20,6 +20,7 @@ import ( "log/slog" "os" "path/filepath" + "slices" "sort" "strings" "sync" @@ -522,9 +523,6 @@ func (s *Store) CaptureAfter(path string, opts CaptureAfterOpts) { } // Update after fingerprint on the most recent snap of this path. updated := false - for i := len(s.curFilesLocked()) - 1; i >= 0; i-- { - // search in cur first, then done reverse - } if s.cur != nil { for i := range s.cur.Files { if NormalizeRelPath(s.root, s.cur.Files[i].Path) != pathKey { @@ -546,8 +544,8 @@ func (s *Store) CaptureAfter(path string, opts CaptureAfterOpts) { // Path might only appear in earlier turns; still record after on earliest? // Ownership after is per-path last write — update the latest checkpoint that // has this path. - for i := len(s.done) - 1; i >= 0; i-- { - c := s.done[i] + for _, v := range slices.Backward(s.done) { + c := v for j := range c.Files { if NormalizeRelPath(s.root, c.Files[j].Path) != pathKey { continue @@ -563,13 +561,6 @@ func (s *Store) CaptureAfter(path string, opts CaptureAfterOpts) { } } -func (s *Store) curFilesLocked() []FileSnap { - if s.cur == nil { - return nil - } - return s.cur.Files -} - // RecordGap appends a coverage gap to the current checkpoint. func (s *Store) RecordGap(gap CoverageGap) { if s == nil { diff --git a/internal/checkpoint/checkpoint_test.go b/internal/checkpoint/checkpoint_test.go index 44e40208cb..73e23ae526 100644 --- a/internal/checkpoint/checkpoint_test.go +++ b/internal/checkpoint/checkpoint_test.go @@ -510,7 +510,7 @@ func BenchmarkRestoreGB18030Encoding(b *testing.B) { b.SetBytes(int64(len(originalRaw))) b.ReportAllocs() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { if err := os.WriteFile(a, editedRaw, 0o644); err != nil { b.Fatal(err) } diff --git a/internal/checkpoint/transaction.go b/internal/checkpoint/transaction.go index 10c7064830..74dac05d56 100644 --- a/internal/checkpoint/transaction.go +++ b/internal/checkpoint/transaction.go @@ -7,6 +7,7 @@ import ( "log/slog" "os" "path/filepath" + "slices" "sort" "strings" "time" @@ -971,8 +972,8 @@ func (s *Store) publishTarget(t *TransactionTarget) error { func (s *Store) compensatePublished(targets []TransactionTarget, stages []FileStage) error { var first error - for i := len(targets) - 1; i >= 0; i-- { - t := targets[i] + for _, v := range slices.Backward(targets) { + t := v if !t.Published { if t.PublishTmp != "" { _ = secureRemove(s.root, t.PublishTmp) @@ -1519,7 +1520,7 @@ func (s *Store) restoreCheckpointBackup(backup []byte) error { } func newID(prefix string) string { - return fmt.Sprintf("%s-%d-%s", prefix, time.Now().UnixNano(), Digest([]byte(fmt.Sprintf("%d", time.Now().UnixNano())))[:8]) + return fmt.Sprintf("%s-%d-%s", prefix, time.Now().UnixNano(), Digest(fmt.Appendf(nil, "%d", time.Now().UnixNano()))[:8]) } // RestoreCheckpointBackupPublic reloads backed-up checkpoints after an undo. diff --git a/internal/cli/bot_test.go b/internal/cli/bot_test.go index 9b82a32b5f..34e18726b3 100644 --- a/internal/cli/bot_test.go +++ b/internal/cli/bot_test.go @@ -5,6 +5,7 @@ import ( "log/slog" "os" "path/filepath" + "slices" "strings" "testing" @@ -492,10 +493,5 @@ func isolateBotUserConfig(t *testing.T) { } func hasTestString(values []string, want string) bool { - for _, value := range values { - if value == want { - return true - } - } - return false + return slices.Contains(values, want) } diff --git a/internal/cli/box.go b/internal/cli/box.go index 7c25b8722e..37481d38e3 100644 --- a/internal/cli/box.go +++ b/internal/cli/box.go @@ -44,10 +44,7 @@ func boxed(lines []string) string { b.WriteString(accent("╭" + bar + "╮")) b.WriteByte('\n') for _, l := range lines { - gap := inner - visibleWidth(l) - 2 - if gap < 0 { - gap = 0 - } + gap := max(inner-visibleWidth(l)-2, 0) b.WriteString(accent("│")) b.WriteByte(' ') b.WriteString(l) diff --git a/internal/cli/chat_ghost_test.go b/internal/cli/chat_ghost_test.go index 339124c728..c492b1ae77 100644 --- a/internal/cli/chat_ghost_test.go +++ b/internal/cli/chat_ghost_test.go @@ -17,7 +17,7 @@ func TestClampWidth(t *testing.T) { // Over width: every resulting line fits, content is preserved. long := strings.Repeat("x", 200) out := clampWidth(long, 40) - for _, line := range strings.Split(out, "\n") { + for line := range strings.SplitSeq(out, "\n") { if visibleWidth(line) > 40 { t.Errorf("clamped line exceeds 40: width=%d", visibleWidth(line)) } diff --git a/internal/cli/chat_render_test.go b/internal/cli/chat_render_test.go index 300b41a5cb..d8eccd7dd3 100644 --- a/internal/cli/chat_render_test.go +++ b/internal/cli/chat_render_test.go @@ -488,7 +488,7 @@ func TestTodoPanelKeepsLastSuccessfulTodoWrite(t *testing.T) { func TestToolProgressTailCap(t *testing.T) { m := newTestChatTUI() m.ingestEvent(event.Event{Kind: event.ToolDispatch, Tool: event.Tool{ID: "b1", Name: "bash", Args: `{"command":"x"}`}}) - for i := 0; i < toolStreamTailLines+5; i++ { + for i := range toolStreamTailLines + 5 { m.ingestEvent(event.Event{Kind: event.ToolProgress, Tool: event.Tool{ID: "b1", Output: "line" + string(rune('A'+i)) + "\n"}}) } block := m.transcript[m.toolStreamIdx] @@ -504,7 +504,7 @@ func TestToolProgressTailCap(t *testing.T) { // long stream — the fix for the O(n²)/multi-GB re-render of the full thought. func TestReasoningViewBounded(t *testing.T) { m := newTestChatTUI() - for i := 0; i < 5000; i++ { + for range 5000 { m.ingestEvent(event.Event{Kind: event.Reasoning, Text: "some thinking text token "}) } if len(m.reasoningView) > reasoningViewMax { diff --git a/internal/cli/chat_tui.go b/internal/cli/chat_tui.go index 382b468971..9d12371a98 100644 --- a/internal/cli/chat_tui.go +++ b/internal/cli/chat_tui.go @@ -8,6 +8,7 @@ import ( "path/filepath" "regexp" "runtime" + "slices" "strconv" "strings" "time" @@ -1669,8 +1670,8 @@ func (m chatTUI) update(msg tea.Msg) (tea.Model, tea.Cmd) { } // "!" runs a shell command directly, bypassing the model. - if strings.HasPrefix(line, "!") { - cmd := strings.TrimPrefix(line, "!") + if after, ok := strings.CutPrefix(line, "!"); ok { + cmd := after if strings.TrimSpace(cmd) == "" { m.input.Reset() m.pastedBlocks = nil @@ -1746,7 +1747,7 @@ func (m chatTUI) update(msg tea.Msg) (tea.Model, tea.Cmd) { // when bash output or reasoning floods in. Capped so a sustained flood // still yields to render periodically. drain: - for drained := 0; drained < maxEventDrain; drained++ { + for range maxEventDrain { select { case e2 := <-m.eventCh: m.noteWatchdogHeartbeat(watchdogAgentSource(e2.Kind)) @@ -2089,10 +2090,7 @@ func chunkLines(s string, n int) []string { } var out []string for i := 0; i < len(lines); i += n { - end := i + n - if end > len(lines) { - end = len(lines) - } + end := min(i+n, len(lines)) out = append(out, strings.Join(lines[i:end], "\n")) } return out @@ -2315,13 +2313,10 @@ func (m *chatTUI) streamReasoning(chunk string) { // positive maxLines keeps only the trailing visual lines (the live view); 0 // renders all (verbose collapse). func reasoningBlock(raw string, width, maxLines int) string { - w := width - len([]rune(connector)) - if w < 8 { - w = 8 - } + w := max(width-len([]rune(connector)), 8) var lines []string - for _, ln := range strings.Split(strings.TrimRight(raw, "\n"), "\n") { - for _, wl := range strings.Split(ansi.Wrap(expandTabs(ln), w, ""), "\n") { + for ln := range strings.SplitSeq(strings.TrimRight(raw, "\n"), "\n") { + for wl := range strings.SplitSeq(ansi.Wrap(expandTabs(ln), w, ""), "\n") { lines = append(lines, dim(wl)) } } @@ -2617,18 +2612,15 @@ func (m *chatTUI) subagentProgressBlock(id string, sp *cliSubagentProgress) stri // subagentPreviewBlock renders a bounded trailing window of a preview channel // as dim, width-wrapped lines carrying a small glyph marker. func subagentPreviewBlock(glyph, raw string, width, maxLines int) string { - w := width - len([]rune(connector)) - if w < 8 { - w = 8 - } + w := max(width-len([]rune(connector)), 8) var lines []string first := true - for _, ln := range strings.Split(strings.TrimRight(raw, "\n"), "\n") { + for ln := range strings.SplitSeq(strings.TrimRight(raw, "\n"), "\n") { if first { ln = glyph + " " + ln first = false } - for _, wl := range strings.Split(ansi.Wrap(expandTabs(ln), w, ""), "\n") { + for wl := range strings.SplitSeq(ansi.Wrap(expandTabs(ln), w, ""), "\n") { lines = append(lines, dim(wl)) } } @@ -2712,7 +2704,7 @@ func (m *chatTUI) collapseToolOutput(id, resultOutput string) { total := len(lines) if total > shellPreviewLines { preview := make([]string, shellPreviewLines+1) - for i := 0; i < shellPreviewLines; i++ { + for i := range shellPreviewLines { preview[i] = dim(clampPlain(lines[i], m.width-len([]rune(connector)))) } preview[shellPreviewLines] = dim(fmt.Sprintf("… %d more lines (Ctrl+B)", total-shellPreviewLines)) @@ -2801,7 +2793,7 @@ func (m *chatTUI) collapseShellSlot(id string, idx int, resultOutput string) { total := len(lines) if total > shellPreviewLines { preview := make([]string, shellPreviewLines+1) - for i := 0; i < shellPreviewLines; i++ { + for i := range shellPreviewLines { preview[i] = dim(clampPlain(lines[i], m.width-len([]rune(connector)))) } preview[shellPreviewLines] = dim(fmt.Sprintf("… %d more lines (Ctrl+B)", total-shellPreviewLines)) @@ -2851,7 +2843,7 @@ func (m *chatTUI) toggleShellOutput() { m.shellExpanded[lastID] = false if total > shellPreviewLines { preview := make([]string, shellPreviewLines+1) - for i := 0; i < shellPreviewLines; i++ { + for i := range shellPreviewLines { preview[i] = dim(clampPlain(lines[i], innerW)) } preview[shellPreviewLines] = dim(fmt.Sprintf("… %d more lines (Ctrl+B)", total-shellPreviewLines)) @@ -2860,12 +2852,9 @@ func (m *chatTUI) toggleShellOutput() { } else { // Expand: show up to shellExpandMaxLines lines. m.shellExpanded[lastID] = true - show := total - if show > shellExpandMaxLines { - show = shellExpandMaxLines - } + show := min(total, shellExpandMaxLines) rendered := make([]string, show) - for i := 0; i < show; i++ { + for i := range show { rendered[i] = dim(clampPlain(lines[i], innerW)) } if total > shellExpandMaxLines { @@ -3140,7 +3129,7 @@ func approvalChoiceLabels(a *event.Approval) []string { choices = fmt.Sprintf(i18n.M.BashPrefixChoices, prefixRule, prefixRule) } var labels []string - for _, line := range strings.Split(choices, "\n") { + for line := range strings.SplitSeq(choices, "\n") { line = strings.TrimSpace(line) if len(line) < 3 || line[0] < '1' || line[0] > '9' || line[1] != '.' { continue @@ -3331,10 +3320,7 @@ func (m chatTUI) View() tea.View { } return v } - boxW := m.width - if boxW < 10 { - boxW = 10 - } + boxW := max(m.width, 10) hideComposer := m.hideComposer() shellMode := strings.HasPrefix(strings.TrimSpace(m.input.Value()), "!") cancelRequested := m.cancelRequested() @@ -3522,7 +3508,7 @@ func compactionCardLines(c event.Compaction) []string { } header := fmt.Sprintf("%s · %d %s · %s", i18n.M.CompactionTitle, c.Messages, i18n.M.CompactionUnit, trigger) lines := []string{accent("◆ " + header)} - for _, ln := range strings.Split(strings.TrimRight(c.Summary, "\n"), "\n") { + for ln := range strings.SplitSeq(strings.TrimRight(c.Summary, "\n"), "\n") { lines = append(lines, dim(" │ "+ln)) } if c.Archive != "" { @@ -3556,10 +3542,7 @@ func (m chatTUI) contextTag() string { } threshold := int(ratio * 100) // Headroom to the compaction point, as a percentage of the window (clamped at 0). - left := threshold - pct - if left < 0 { - left = 0 - } + left := max(threshold-pct, 0) body := fmt.Sprintf("%s ctx (%d%%) · %d%% to compact", shortTokens(used), pct, left) switch { case pct >= threshold: @@ -3674,10 +3657,7 @@ func shortTokens(n int) string { // renderApprovalBanner is the slim notice shown above the input while a tool // call (or a plan) awaits the user's decision. func (m chatTUI) renderApprovalBanner() string { - w := m.width - if w < 10 { - w = 10 - } + w := max(m.width, 10) if m.pendingApproval == nil { return "" } @@ -3736,10 +3716,7 @@ func approvalSubjectBody(full, preview string, width int) string { if full == "" || full == preview { return "" } - wrapWidth := width - 4 - if wrapWidth < 20 { - wrapWidth = 20 - } + wrapWidth := max(width-4, 20) lines := strings.Split(wrapStatusLine(full, wrapWidth), "\n") if len(lines) > maxApprovalSubjectLines { lines = lines[:maxApprovalSubjectLines] @@ -3878,10 +3855,7 @@ func todoPanelWindow(todos []todoPanelTodo) (int, int) { if active < 0 { return 0, todoPanelMaxRows } - start := active - todoPanelMaxRows/2 - if start < 0 { - start = 0 - } + start := max(active-todoPanelMaxRows/2, 0) if maxStart := len(todos) - todoPanelMaxRows; start > maxStart { start = maxStart } @@ -4010,13 +3984,7 @@ func (m *chatTUI) growInputToFit() { if m.input.DynamicHeight { return } - lines := strings.Count(m.input.Value(), "\n") + 1 - if lines < 1 { - lines = 1 - } - if lines > maxInputRows { - lines = maxInputRows - } + lines := min(max(strings.Count(m.input.Value(), "\n")+1, 1), maxInputRows) if lines != m.input.Height() { m.input.SetHeight(lines) } @@ -4952,7 +4920,7 @@ func (m *chatTUI) runCopyCommand(input string) tea.Cmd { // firstLine returns the first non-empty line of s, truncated to 80 runes. func firstLine(s string) string { - for _, line := range strings.Split(s, "\n") { + for line := range strings.SplitSeq(s, "\n") { if t := strings.TrimSpace(line); t != "" { runes := []rune(t) if len(runes) > 80 { @@ -4969,8 +4937,8 @@ func firstLine(s string) string { // The result is chronological (oldest first). func copyAssistantParts(msgs []provider.Message) []string { lastUserIdx := -1 - for i := len(msgs) - 1; i >= 0; i-- { - if msgs[i].Role == provider.RoleUser { + for i, v := range slices.Backward(msgs) { + if v.Role == provider.RoleUser { lastUserIdx = i break } diff --git a/internal/cli/chat_tui_paste.go b/internal/cli/chat_tui_paste.go index 71b3d8e12e..5702abeb38 100644 --- a/internal/cli/chat_tui_paste.go +++ b/internal/cli/chat_tui_paste.go @@ -8,6 +8,7 @@ import ( "path/filepath" "regexp" "runtime" + "slices" "strconv" "strings" @@ -129,11 +130,11 @@ func recoverOrphanedPasteLabelsFromHistory(sent string, knownBlocks []pastedBloc var recovered string found := false ambiguous := false - for i := len(history) - 1; i >= 0; i-- { - if history[i].Role != provider.RoleUser { + for _, v := range slices.Backward(history) { + if v.Role != provider.RoleUser { continue } - for _, body := range expandedPasteBodies(history[i].Content, label) { + for _, body := range expandedPasteBodies(v.Content, label) { if !found { recovered = body found = true @@ -246,10 +247,7 @@ func (m *chatTUI) takeNextPasteID() int { if m.ctrl != nil { m.syncPasteIDStateFromHistory(m.ctrl.History()) } - candidate := m.nextPasteID - if candidate < 1 { - candidate = 1 - } + candidate := max(m.nextPasteID, 1) if m.usedPasteIDs == nil { m.usedPasteIDs = make(map[int]struct{}) } @@ -519,7 +517,7 @@ func splitPastePathTokens(s string) []string { b.Reset() } } - for i := 0; i < len(s); i++ { + for i := range len(s) { ch := s[i] switch { case escaped: @@ -548,7 +546,7 @@ func splitPastePathTokens(s string) []string { func nonEmptyPasteLines(text string) []string { var out []string - for _, line := range strings.Split(strings.ReplaceAll(text, "\r\n", "\n"), "\n") { + for line := range strings.SplitSeq(strings.ReplaceAll(text, "\r\n", "\n"), "\n") { line = strings.TrimSpace(line) if line != "" { out = append(out, line) @@ -629,7 +627,7 @@ func pastedImagePathForOS(src, goos string) (string, bool) { func hasUnescapedPathWhitespace(s string) bool { escaped := false - for i := 0; i < len(s); i++ { + for i := range len(s) { ch := s[i] if escaped { escaped = false diff --git a/internal/cli/chat_tui_test.go b/internal/cli/chat_tui_test.go index c67ab41376..5bc317b7cf 100644 --- a/internal/cli/chat_tui_test.go +++ b/internal/cli/chat_tui_test.go @@ -8,6 +8,7 @@ import ( "os/exec" "path/filepath" "reflect" + "slices" "strings" "testing" @@ -547,7 +548,7 @@ func TestTranscriptResizeRerendersCommittedMarkdownAtNewWidth(t *testing.T) { newLines := strings.Count(newRendered, "\n") + 1 ruleWidth := 0 - for _, line := range strings.Split(newRendered, "\n") { + for line := range strings.SplitSeq(newRendered, "\n") { trimmed := strings.TrimSpace(line) if trimmed != "" && strings.Trim(trimmed, "─") == "" { ruleWidth = visibleWidth(trimmed) @@ -988,7 +989,7 @@ func TestRewindPickerWindowsLongSession(t *testing.T) { if strings.Contains(card, "more") { t.Fatalf("short session must not show more markers: %q", card) } - for i := 0; i < 4; i++ { + for i := range 4 { if !strings.Contains(card, fmt.Sprintf("turn %d", i)) { t.Fatalf("short session row %d missing: %q", i, card) } @@ -1497,13 +1498,7 @@ func TestInsertNewlineKeyBinding(t *testing.T) { ctrl := control.New(control.Options{}) m := newChatTUI(ctrl, "", make(chan event.Event, 1), 80) keys := m.input.KeyMap.InsertNewline.Keys() - found := false - for _, k := range keys { - if k == "shift+enter" { - found = true - break - } - } + found := slices.Contains(keys, "shift+enter") if !found { t.Errorf("newChatTUI InsertNewline should include shift+enter, got %v", keys) } @@ -1519,7 +1514,7 @@ func TestCtrlHomeEndScrollKeyBindings(t *testing.T) { } cur := adv(newChatTUI(ctrl, "", ch, 80), tea.WindowSizeMsg{Width: 80, Height: 8}) - for i := 0; i < 12; i++ { + for range 12 { cur = adv(cur, notice) } // Viewport should be at the bottom after output. @@ -1550,7 +1545,7 @@ func TestMouseWheelAndPageKeysScrollTranscript(t *testing.T) { } cur := adv(newChatTUI(ctrl, "", ch, 80), tea.WindowSizeMsg{Width: 80, Height: 10}) - for i := 0; i < 40; i++ { + for range 40 { cur = adv(cur, notice) } if !cur.viewport.AtBottom() { @@ -1593,7 +1588,7 @@ func TestRunningStreamPreservesScrolledReadingPosition(t *testing.T) { } cur := adv(newChatTUI(ctrl, "", ch, 80), tea.WindowSizeMsg{Width: 80, Height: 10}) - for i := 0; i < 40; i++ { + for range 40 { cur = adv(cur, notice) } cur.state = tuiRunning @@ -1630,7 +1625,7 @@ func TestTranscriptScrollbarClickAndDrag(t *testing.T) { } cur := adv(newChatTUI(ctrl, "", ch, 80), tea.WindowSizeMsg{Width: 80, Height: 10}) - for i := 0; i < 40; i++ { + for range 40 { cur = adv(cur, notice) } cur.viewport.GotoTop() @@ -3028,7 +3023,7 @@ func TestTranscriptTailFollow(t *testing.T) { notice := agentEventMsg(event.Event{Kind: event.Notice, Level: event.LevelInfo, Text: "line"}) cur := adv(newChatTUI(ctrl, "", make(chan event.Event, 1), 80), tea.WindowSizeMsg{Width: 80, Height: 8}) - for i := 0; i < 12; i++ { // overflow the short viewport so there's room to scroll + for range 12 { // overflow the short viewport so there's room to scroll cur = adv(cur, notice) } if !cur.viewport.AtBottom() { @@ -3061,7 +3056,7 @@ func TestEmptyEnterScrollsToBottom(t *testing.T) { // idle state t.Run("idle", func(t *testing.T) { cur := adv(newChatTUI(ctrl, "", ch, 80), tea.WindowSizeMsg{Width: 80, Height: 8}) - for i := 0; i < 12; i++ { + for range 12 { cur = adv(cur, notice) } // Scroll up to leave the bottom. @@ -3079,7 +3074,7 @@ func TestEmptyEnterScrollsToBottom(t *testing.T) { // running state t.Run("running", func(t *testing.T) { cur := adv(newChatTUI(ctrl, "", ch, 80), tea.WindowSizeMsg{Width: 80, Height: 8}) - for i := 0; i < 12; i++ { + for range 12 { cur = adv(cur, notice) } cur.state = tuiRunning @@ -3110,7 +3105,7 @@ func TestForceGotoBottomScrollsWithoutTranscriptChange(t *testing.T) { } cur := next(newChatTUI(ctrl, "", ch, 80), tea.WindowSizeMsg{Width: 80, Height: 8}) - for i := 0; i < 12; i++ { + for range 12 { cur = next(cur, notice) } if !cur.viewport.AtBottom() { @@ -3151,7 +3146,7 @@ func TestSessionSwitchSuppressesOneClearScreen(t *testing.T) { } cur := next(newChatTUI(ctrl, "", ch, 80), tea.WindowSizeMsg{Width: 80, Height: 8}) - for i := 0; i < 12; i++ { + for range 12 { cur = next(cur, notice) } cur = next(cur, tea.MouseWheelMsg{Button: tea.MouseWheelUp}) @@ -4062,10 +4057,7 @@ func TestTruncateSubject(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got := truncateSubject(tc.input, tc.width) - wantMax := tc.width - 28 - if wantMax < 16 { - wantMax = 16 - } + wantMax := max(tc.width-28, 16) w := ansi.StringWidth(got) if w > wantMax { t.Errorf("truncateSubject(%q, %d) = %q (width %d), want visible width <= %d", tc.input, tc.width, got, w, wantMax) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index a4d9fa5cc9..083b4a3b4c 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -19,6 +19,7 @@ import ( "os" "os/signal" "path/filepath" + "slices" "sort" "strconv" "strings" @@ -1446,7 +1447,7 @@ func prepareNativeScrollback(w io.Writer, rows int) { } func reserveNativeScrollbackFrame(w io.Writer, rows int) { - for i := 0; i < rows; i++ { + for range rows { fmt.Fprintln(w) } } @@ -1783,12 +1784,7 @@ func buildFamilyEntry(probe config.ProviderEntry, selected []string) config.Prov } func containsString(xs []string, v string) bool { - for _, x := range xs { - if x == v { - return true - } - } - return false + return slices.Contains(xs, v) } // filterStaleCustomEntries drops the wizard's own magic-name entries @@ -2317,7 +2313,7 @@ func appendEnv(path string, lines []string) error { var kept []string if data, err := fileencoding.ReadFileUTF8(path); err == nil { - for _, raw := range strings.Split(string(data), "\n") { + for raw := range strings.SplitSeq(string(data), "\n") { trimmed := strings.TrimSpace(raw) check := strings.TrimPrefix(trimmed, "export ") if k, _, ok := strings.Cut(check, "="); ok && target[strings.TrimSpace(k)] { diff --git a/internal/cli/cli_flags.go b/internal/cli/cli_flags.go index 23cc930706..6a6777177f 100644 --- a/internal/cli/cli_flags.go +++ b/internal/cli/cli_flags.go @@ -198,7 +198,7 @@ func looksLikeMachineSessionID(query string) bool { if len(hexPart) != 32 { return false } - for i := 0; i < len(hexPart); i++ { + for i := range len(hexPart) { c := hexPart[i] if (c < '0' || c > '9') && (c < 'a' || c > 'f') { return false diff --git a/internal/cli/complete.go b/internal/cli/complete.go index 416a9d78e3..2244a9dc21 100644 --- a/internal/cli/complete.go +++ b/internal/cli/complete.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "sort" "strings" @@ -144,8 +145,8 @@ func renameSlashItem(items []compItem, oldLabel, newLabel string) []compItem { continue } items[i].label = newLabel - if strings.HasPrefix(items[i].insert, oldLabel) { - items[i].insert = newLabel + strings.TrimPrefix(items[i].insert, oldLabel) + if after, ok := strings.CutPrefix(items[i].insert, oldLabel); ok { + items[i].insert = newLabel + after } break } @@ -225,10 +226,7 @@ func (m *chatTUI) inputCursorByteOffset() int { // cur.X is screen-relative and includes the "❯ " prompt // gutter (composerPromptWidth columns). Subtract it so // we measure content columns only. - col := cur.X - composerPromptWidth - if col < 0 { - col = 0 - } + col := max(cur.X-composerPromptWidth, 0) visual := 0 for _, cell := range row.cells { w := rw.RuneWidth(cell.r) @@ -505,13 +503,7 @@ func activeAtToken(val string, cursor int) (at, end int, query string, ok bool) case '@': if i == 0 || val[i-1] == ' ' || val[i-1] == '\t' || val[i-1] == '\n' { end = tokenEnd(val, i+1) - queryEnd := cursor - if queryEnd < i+1 { - queryEnd = i + 1 - } - if queryEnd > end { - queryEnd = end - } + queryEnd := min(max(cursor, i+1), end) return i, end, val[i+1 : queryEnd], true } return 0, 0, "", false @@ -608,10 +600,7 @@ func (m *chatTUI) fileItems(token string) []compItem { for _, it := range items { seen[strings.TrimPrefix(it.insert, "@")] = true } - remaining := maxCompItems - len(items) - if remaining > maxFileSearchItems { - remaining = maxFileSearchItems - } + remaining := min(maxCompItems-len(items), maxFileSearchItems) results := m.searchFileRefs(fsFrag) if len(results) > remaining { results = results[:remaining] @@ -669,12 +658,7 @@ func (m *chatTUI) isMCPServer(name string) bool { if m.host == nil { return false } - for _, s := range m.host.ServerNames() { - if s == name { - return true - } - } - return false + return slices.Contains(m.host.ServerNames(), name) } // resourceItems lists MCP resources as @server:uri completions. When server is @@ -835,18 +819,9 @@ func (m chatTUI) renderCompletion() string { items := m.completion.items start := 0 if len(items) > maxCompRows { - start = m.completion.sel - maxCompRows/2 - if start < 0 { - start = 0 - } - if start > len(items)-maxCompRows { - start = len(items) - maxCompRows - } - } - end := start + maxCompRows - if end > len(items) { - end = len(items) + start = min(max(m.completion.sel-maxCompRows/2, 0), len(items)-maxCompRows) } + end := min(start+maxCompRows, len(items)) var b strings.Builder for i := start; i < end; i++ { diff --git a/internal/cli/complete_test.go b/internal/cli/complete_test.go index bb62f9ca81..4f618a1362 100644 --- a/internal/cli/complete_test.go +++ b/internal/cli/complete_test.go @@ -367,7 +367,7 @@ func TestFileItemsSearchRespectsMenuCap(t *testing.T) { defer os.Chdir(orig) dir := t.TempDir() - for i := 0; i < maxCompItems; i++ { + for i := range maxCompItems { writeAt(t, dir, filepath.Join("aa-dir-"+fmt.Sprintf("%03d", i), "file.txt"), "x") } writeAt(t, dir, "nested/aa-deep.js", "y") diff --git a/internal/cli/composer_selection.go b/internal/cli/composer_selection.go index 655a4c1d81..397e92d71e 100644 --- a/internal/cli/composer_selection.go +++ b/internal/cli/composer_selection.go @@ -383,10 +383,7 @@ func (m *chatTUI) composerCaretAt(screenX, screenY int, clamp bool) (composerCar relY = m.input.Height() - 1 } rows := m.composerRows() - visualRow := m.composerViewOffset() + relY - if visualRow < 0 { - visualRow = 0 - } + visualRow := max(m.composerViewOffset()+relY, 0) if visualRow >= len(rows) { visualRow = len(rows) - 1 } @@ -398,7 +395,7 @@ func (m *chatTUI) setComposerCursor(offset int) { rows := m.composerRows() caret := composerCaretForOffset(rows, offset) m.input.MoveToBegin() - for i := 0; i < caret.visualRow; i++ { + for range caret.visualRow { m.input.CursorDown() } m.input.SetCursorColumn(caret.logicalCol) diff --git a/internal/cli/consecutive_tool_markers_test.go b/internal/cli/consecutive_tool_markers_test.go index 80206e3f3b..8cd49e711d 100644 --- a/internal/cli/consecutive_tool_markers_test.go +++ b/internal/cli/consecutive_tool_markers_test.go @@ -21,7 +21,7 @@ func TestParallelBashMarkersKeepOwnLineCount(t *testing.T) { m.ingestEvent(event.Event{Kind: event.ToolDispatch, Tool: event.Tool{ID: id, Name: "bash", Args: `{"command":"ls"}`, Partial: false}}) } for _, id := range ids { - for i := 0; i < 22; i++ { + for range 22 { m.ingestEvent(event.Event{Kind: event.ToolProgress, Tool: event.Tool{ID: id, Output: "line\n"}}) } } diff --git a/internal/cli/diffview.go b/internal/cli/diffview.go index 23ec0fcf41..e4dcd50c0b 100644 --- a/internal/cli/diffview.go +++ b/internal/cli/diffview.go @@ -144,19 +144,13 @@ func diffBody(d event.FileDiff, path string, width, maxLines int) []string { // bar mid-line — and padded to the bar width so it runs edge to edge. func diffBar(sign byte, code, path string, width int, bg, signFg string, lineNo, gw int) string { gutter := dim(lpad(strconv.Itoa(lineNo), gw)) - barW := width - 2 - gw - 1 - if barW < 4 { - barW = 4 - } + barW := max(width-2-gw-1, 4) code = clampPlain(code, barW-2) if !colorOn() { return " " + gutter + " " + string(sign) + " " + code } hl := reapplyBG(highlightCode(path, code), bg) - pad := barW - 2 - visibleWidth(code) - if pad < 0 { - pad = 0 - } + pad := max(barW-2-visibleWidth(code), 0) return " " + gutter + " " + bg + signFg + string(sign) + ansiReset + bg + " " + hl + strings.Repeat(" ", pad) + ansiReset } @@ -232,7 +226,7 @@ func expandTabs(s string) string { for _, r := range s { if r == '\t' { n := tabWidth - col%tabWidth - for i := 0; i < n; i++ { + for range n { b.WriteByte(' ') } col += n diff --git a/internal/cli/diffview_test.go b/internal/cli/diffview_test.go index 62c5994b82..dce587667b 100644 --- a/internal/cli/diffview_test.go +++ b/internal/cli/diffview_test.go @@ -26,7 +26,7 @@ func TestDiffBodyDropsHeadersKeepsLineNumbers(t *testing.T) { func TestDiffBodyFolds(t *testing.T) { var b strings.Builder b.WriteString("--- a/x\n+++ b/x\n@@ -1,8 +1,8 @@\n") - for i := 0; i < 8; i++ { + for range 8 { b.WriteString("+line\n") } body := diffBody(event.FileDiff{Diff: b.String()}, "x", 80, 5) diff --git a/internal/cli/extension_surface.go b/internal/cli/extension_surface.go index dfc28d7091..b88943632b 100644 --- a/internal/cli/extension_surface.go +++ b/internal/cli/extension_surface.go @@ -98,7 +98,7 @@ func extensionCardLines(pluginID string, c *event.ExtensionCardView, width int) body = c.Markdown } } - for _, ln := range strings.Split(strings.TrimRight(body, "\n"), "\n") { + for ln := range strings.SplitSeq(strings.TrimRight(body, "\n"), "\n") { if ln == "" { lines = append(lines, "") continue @@ -131,7 +131,7 @@ func extensionFormLines(pluginID string, f *event.ExtensionFormView) []string { title = pluginID } lines := []string{accent("◆ " + title)} - for _, ln := range strings.Split(strings.TrimRight(f.Message, "\n"), "\n") { + for ln := range strings.SplitSeq(strings.TrimRight(f.Message, "\n"), "\n") { if ln == "" { continue } diff --git a/internal/cli/gitstatus.go b/internal/cli/gitstatus.go index 3c4d2a0348..8fc7a09469 100644 --- a/internal/cli/gitstatus.go +++ b/internal/cli/gitstatus.go @@ -84,7 +84,7 @@ func runGit(ctx context.Context, cwd string, args ...string) (string, error) { } func parseGitNumstat(out string) (added int, removed int) { - for _, line := range strings.Split(strings.TrimSpace(out), "\n") { + for line := range strings.SplitSeq(strings.TrimSpace(out), "\n") { if line == "" { continue } @@ -108,7 +108,7 @@ func parseGitNumstat(out string) (added int, removed int) { func countUntracked(out string) int { n := 0 - for _, line := range strings.Split(strings.TrimRight(out, "\n"), "\n") { + for line := range strings.SplitSeq(strings.TrimRight(out, "\n"), "\n") { if strings.HasPrefix(line, "?? ") { n++ } diff --git a/internal/cli/machine_identity_test.go b/internal/cli/machine_identity_test.go index 8c1ac4dda8..5f9716c075 100644 --- a/internal/cli/machine_identity_test.go +++ b/internal/cli/machine_identity_test.go @@ -35,13 +35,11 @@ func TestMachineIdentityKeyInitializesOnceAcrossConcurrentReaders(t *testing.T) const readers = 16 results := make(chan result, readers) var wg sync.WaitGroup - for i := 0; i < readers; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range readers { + wg.Go(func() { key, err := loadMachineIdentityKey() results <- result{key: key, err: err} - }() + }) } wg.Wait() close(results) diff --git a/internal/cli/mcp_manager.go b/internal/cli/mcp_manager.go index 5ec0376351..934a20c6d4 100644 --- a/internal/cli/mcp_manager.go +++ b/internal/cli/mcp_manager.go @@ -428,10 +428,7 @@ func visibleRange(total, sel, limit int) (int, int) { if sel >= total { sel = total - 1 } - start := sel - limit/2 - if start < 0 { - start = 0 - } + start := max(sel-limit/2, 0) if start+limit > total { start = total - limit } diff --git a/internal/cli/mcp_manager_view.go b/internal/cli/mcp_manager_view.go index 195b11c745..d577377292 100644 --- a/internal/cli/mcp_manager_view.go +++ b/internal/cli/mcp_manager_view.go @@ -177,10 +177,7 @@ func (p *mcpManager) renderTools(width int) string { if len(v.ToolList) == 0 { b.WriteString(viewMeta("Current connection did not return tool details.") + "\n") } else { - limit := len(v.ToolList) - if limit > mcpToolMaxRows { - limit = mcpToolMaxRows - } + limit := min(len(v.ToolList), mcpToolMaxRows) for _, t := range v.ToolList[:limit] { desc := t.Description if t.SchemaError != "" { diff --git a/internal/cli/mcp_test.go b/internal/cli/mcp_test.go index 5f1f0e61cd..192c4a0b0a 100644 --- a/internal/cli/mcp_test.go +++ b/internal/cli/mcp_test.go @@ -430,7 +430,7 @@ func TestRenderMCPStatusGroupsAndCompactsResources(t *testing.T) { func TestRenderMCPStatusCapsLongSections(t *testing.T) { var resources []plugin.Resource - for i := 0; i < mcpMaxItemsPerSection+2; i++ { + for range mcpMaxItemsPerSection + 2 { resources = append(resources, plugin.Resource{Server: "fs", URI: "file:///tmp/resource.md"}) } got := renderMCPStatus(80, @@ -648,7 +648,7 @@ func TestRenderMCPManagerListCompactsLongNames(t *testing.T) { {Name: "@modelcontextprotocol/server-sequential-thinking", Transport: "stdio", Status: "deferred", Configured: true, Tier: "background"}, }}} got := p.renderList(80) - for _, line := range strings.Split(got, "\n") { + for line := range strings.SplitSeq(got, "\n") { if visibleWidth(line) > 80 { t.Fatalf("line exceeds width 80 (%d): %q\n%s", visibleWidth(line), line, got) } @@ -790,7 +790,7 @@ func TestRenderMCPManagerDetailCompactsConfigPath(t *testing.T) { }, } got := p.renderDetail(80) - for _, line := range strings.Split(got, "\n") { + for line := range strings.SplitSeq(got, "\n") { if visibleWidth(line) > 80 { t.Fatalf("detail line exceeds width 80 (%d): %q\n%s", visibleWidth(line), line, got) } diff --git a/internal/cli/mcp_view.go b/internal/cli/mcp_view.go index 0e8b79be2a..6a4c009d47 100644 --- a/internal/cli/mcp_view.go +++ b/internal/cli/mcp_view.go @@ -87,10 +87,7 @@ func writeMCPServer(b *strings.Builder, width int, s plugin.ServerStatus, prompt } if len(invalidTools) > 0 { b.WriteString(viewSubhead(" unavailable tools") + "\n") - limit := len(invalidTools) - if limit > mcpMaxItemsPerSection { - limit = mcpMaxItemsPerSection - } + limit := min(len(invalidTools), mcpMaxItemsPerSection) for _, t := range invalidTools[:limit] { writeMCPItem(b, width, " ", sanitizeExternalDisplayText(t.Name), sanitizeExternalDisplayText(t.SchemaError)) } @@ -109,10 +106,7 @@ func writeMCPToolList(b *strings.Builder, width int, s plugin.ServerStatus, tool return } b.WriteString(viewSubhead(" tools") + "\n") - limit := len(tools) - if limit > mcpMaxItemsPerSection { - limit = mcpMaxItemsPerSection - } + limit := min(len(tools), mcpMaxItemsPerSection) src := sanitizeExternalDisplayText(s.ConfigSource) for _, t := range tools[:limit] { detail := sanitizeExternalDisplayText(t.Description) @@ -162,10 +156,7 @@ func writeMCPFailure(b *strings.Builder, width int, f plugin.Failure) { func writeMCPPromptList(b *strings.Builder, width int, prompts []plugin.Prompt) { b.WriteString(viewSubhead(" prompts") + "\n") - limit := len(prompts) - if limit > mcpMaxItemsPerSection { - limit = mcpMaxItemsPerSection - } + limit := min(len(prompts), mcpMaxItemsPerSection) for _, p := range prompts[:limit] { writeMCPItem(b, width, " ", "/"+sanitizeExternalDisplayText(p.Name), sanitizeExternalDisplayText(p.Description)) } @@ -176,10 +167,7 @@ func writeMCPPromptList(b *strings.Builder, width int, prompts []plugin.Prompt) func writeMCPResourceList(b *strings.Builder, width int, resources []plugin.Resource) { b.WriteString(viewSubhead(" resources") + "\n") - limit := len(resources) - if limit > mcpMaxItemsPerSection { - limit = mcpMaxItemsPerSection - } + limit := min(len(resources), mcpMaxItemsPerSection) for _, r := range resources[:limit] { label := sanitizeExternalDisplayText(r.Name) if label == "" { diff --git a/internal/cli/md.go b/internal/cli/md.go index 4583ee5047..3dd220a742 100644 --- a/internal/cli/md.go +++ b/internal/cli/md.go @@ -215,10 +215,7 @@ func (r *mdRenderer) renderBlock(buf *strings.Builder, node ast.Node, src []byte case *extast.Table: r.renderTable(buf, n, src, indent) case *ast.ThematicBreak: - w := r.width - indent - if w < 8 { - w = 8 - } + w := max(r.width-indent, 8) buf.WriteString(strings.Repeat(" ", indent)) buf.WriteString(dim(strings.Repeat("─", w))) buf.WriteString("\n\n") @@ -256,7 +253,7 @@ func (r *mdRenderer) renderInlineBlock(buf *strings.Builder, n ast.Node, src []b inline := r.collectInline(n, src) prefix := strings.Repeat(" ", indent) wrapped := wrapAnsi(inline, r.width-indent) - for _, line := range strings.Split(wrapped, "\n") { + for line := range strings.SplitSeq(wrapped, "\n") { buf.WriteString(prefix) buf.WriteString(line) buf.WriteString("\n") @@ -311,7 +308,7 @@ func (r *mdRenderer) renderList(buf *strings.Builder, n *ast.List, src []byte, i func (r *mdRenderer) renderFenced(buf *strings.Builder, n ast.Node, src []byte, indent int) { prefix := strings.Repeat(" ", indent) + dim("│ ") - for i := 0; i < n.Lines().Len(); i++ { + for i := range n.Lines().Len() { l := n.Lines().At(i) line := strings.TrimRight(string(l.Value(src)), "\n") buf.WriteString(prefix) @@ -325,7 +322,7 @@ func (r *mdRenderer) renderBlockquote(buf *strings.Builder, n *ast.Blockquote, s var inner strings.Builder r.renderBlocks(&inner, n, src, 0) prefix := strings.Repeat(" ", indent) + dim("▎ ") - for _, line := range strings.Split(strings.TrimRight(inner.String(), "\n"), "\n") { + for line := range strings.SplitSeq(strings.TrimRight(inner.String(), "\n"), "\n") { buf.WriteString(prefix) buf.WriteString(dim(line)) buf.WriteString("\n") @@ -448,20 +445,14 @@ func (r *mdRenderer) renderTable(buf *strings.Builder, n *extast.Table, src []by // widths + separators (3 chars each) + indent. Distribute the budget // proportionally to the natural widths so columns with rich content // keep more space than narrow ones. - available := r.width - indent - 3*(cols-1) - if available < cols*3 { - available = cols * 3 - } + available := max(r.width-indent-3*(cols-1), cols*3) total := 0 for _, w := range widths { total += w } if total > available { for i := range widths { - widths[i] = widths[i] * available / total - if widths[i] < 3 { - widths[i] = 3 - } + widths[i] = max(widths[i]*available/total, 3) } } @@ -493,7 +484,7 @@ func (r *mdRenderer) renderTableRow(buf *strings.Builder, prefix, sep string, ce cols := len(widths) wrapped := make([][]string, cols) maxLines := 1 - for i := 0; i < cols; i++ { + for i := range cols { var text string if i < len(cells) { text = cells[i] @@ -503,9 +494,9 @@ func (r *mdRenderer) renderTableRow(buf *strings.Builder, prefix, sep string, ce maxLines = len(wrapped[i]) } } - for line := 0; line < maxLines; line++ { + for line := range maxLines { buf.WriteString(prefix) - for i := 0; i < cols; i++ { + for i := range cols { if i > 0 { buf.WriteString(sep) } diff --git a/internal/cli/mouse_reenable.go b/internal/cli/mouse_reenable.go index f02c068725..5a051615ad 100644 --- a/internal/cli/mouse_reenable.go +++ b/internal/cli/mouse_reenable.go @@ -52,10 +52,7 @@ func (m *chatTUI) armMouseReenableTimer(now time.Time) tea.Cmd { if m.mouseReenableTimerArmed { return nil } - wait := mouseReenableMinInterval - now.Sub(m.lastMouseReenable) - if wait < 0 { - wait = 0 - } + wait := max(mouseReenableMinInterval-now.Sub(m.lastMouseReenable), 0) m.mouseReenableTimerArmed = true return tea.Tick(wait, func(time.Time) tea.Msg { return mouseReenableMsg{} diff --git a/internal/cli/quick_picker.go b/internal/cli/quick_picker.go index 6c780d0ff4..9c2ecea873 100644 --- a/internal/cli/quick_picker.go +++ b/internal/cli/quick_picker.go @@ -165,10 +165,7 @@ func quickPickerWindow(total, selected int) (int, int) { if total <= quickPickerMaxVisible { return 0, total } - start := selected - quickPickerMaxVisible/2 - if start < 0 { - start = 0 - } + start := max(selected-quickPickerMaxVisible/2, 0) if maxStart := total - quickPickerMaxVisible; start > maxStart { start = maxStart } diff --git a/internal/cli/quick_picker_test.go b/internal/cli/quick_picker_test.go index 1ed8933ed2..73ba8b2bb9 100644 --- a/internal/cli/quick_picker_test.go +++ b/internal/cli/quick_picker_test.go @@ -83,7 +83,7 @@ func TestQuickPickerRendersWithinNarrowPanel(t *testing.T) { }}, } out := p.render(32) - for _, line := range strings.Split(out, "\n") { + for line := range strings.SplitSeq(out, "\n") { if got := visibleWidth(line); got > 32 { t.Fatalf("rendered line width = %d, want <= 32: %q", got, line) } diff --git a/internal/cli/remote_cmd_test.go b/internal/cli/remote_cmd_test.go index b484c1dbd3..994c4290b0 100644 --- a/internal/cli/remote_cmd_test.go +++ b/internal/cli/remote_cmd_test.go @@ -86,7 +86,7 @@ func TestRemoteRemoveCleansGeneratedCredentialsButKeepsUserManagedOnes(t *testin if _, err := config.SetCredential(key, value); err != nil { t.Fatal(err) } - key := key + t.Cleanup(func() { _ = config.RemoveCredential(key) }) } if err := editUserConfig(func(c *config.Config) error { diff --git a/internal/cli/resume_test.go b/internal/cli/resume_test.go index 95271f78f2..e8e32ee69a 100644 --- a/internal/cli/resume_test.go +++ b/internal/cli/resume_test.go @@ -182,7 +182,7 @@ func TestRunResumeKeepsCompletedIndexStableAcrossRecoveryGC(t *testing.T) { func TestCapResumeSessionGroupsDoesNotSplitRecoveryFamily(t *testing.T) { base := time.Date(2026, 8, 3, 12, 0, 0, 0, time.UTC) sessions := make([]agent.SessionInfo, 0, 12) - for i := 0; i < 9; i++ { + for i := range 9 { sessions = append(sessions, agent.SessionInfo{ Path: filepath.Join("/sessions", "standalone-"+strconv.Itoa(i)+".jsonl"), ModTime: base.Add(time.Duration(20-i) * time.Minute), @@ -337,7 +337,7 @@ func TestResumeDispatchSwitchesAndReplays(t *testing.T) { func TestResumeWhileScrolledUpPinsViewportToBottom(t *testing.T) { dir := t.TempDir() active := agent.NewSession("sys") - for i := 0; i < 18; i++ { + for i := range 18 { active.Add(provider.Message{Role: provider.RoleUser, Content: "active prompt " + strconv.Itoa(i)}) } exec := agent.New(nil, nil, active, agent.Options{}, event.Discard) diff --git a/internal/cli/run_metrics.go b/internal/cli/run_metrics.go index 2732f45600..334740b9b4 100644 --- a/internal/cli/run_metrics.go +++ b/internal/cli/run_metrics.go @@ -2,6 +2,7 @@ package cli import ( "encoding/json" + "maps" "os" "strings" "sync" @@ -154,9 +155,7 @@ func cloneCounts(in map[string]int) map[string]int { return nil } out := make(map[string]int, len(in)) - for k, v := range in { - out[k] = v - } + maps.Copy(out, in) return out } @@ -165,9 +164,7 @@ func cloneSourceUsage(in map[string]SourceUsage) map[string]SourceUsage { return nil } out := make(map[string]SourceUsage, len(in)) - for k, v := range in { - out[k] = v - } + maps.Copy(out, in) return out } diff --git a/internal/cli/run_metrics_partial_test.go b/internal/cli/run_metrics_partial_test.go index a2b7194d18..83e6fb30eb 100644 --- a/internal/cli/run_metrics_partial_test.go +++ b/internal/cli/run_metrics_partial_test.go @@ -89,7 +89,7 @@ func TestSnapshotsAreThrottled(t *testing.T) { snapshotEvery: time.Minute, clock: func() time.Time { return now }, } - for i := 0; i < 50; i++ { + for range 50 { s.Emit(usageEvent(event.UsageSourceExecutor, 10, 1)) } raw, err := os.ReadFile(s.partialPath) @@ -189,26 +189,22 @@ func TestConcurrentEmitAndSnapshotAreRaceFree(t *testing.T) { const emitters, each = 8, 50 var wg sync.WaitGroup - for i := 0; i < emitters; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < each; j++ { + for range emitters { + wg.Go(func() { + for range each { s.Emit(usageEventWithCacheReason("compact_auto")) s.Emit(event.Event{Kind: event.ToolResult, Tool: event.Tool{Name: "bash"}}) } - }() + }) } - wg.Add(1) - go func() { - defer wg.Done() - for i := 0; i < 200; i++ { + wg.Go(func() { + for range 200 { if _, err := json.Marshal(s.Snapshot()); err != nil { t.Errorf("marshal snapshot: %v", err) return } } - }() + }) wg.Wait() m := s.Snapshot() diff --git a/internal/cli/scroll_state_test.go b/internal/cli/scroll_state_test.go index 9ba50a8c83..ec6f8cf36a 100644 --- a/internal/cli/scroll_state_test.go +++ b/internal/cli/scroll_state_test.go @@ -24,7 +24,7 @@ func TestModalOpenDoesNotDisableTailFollow(t *testing.T) { notice := agentEventMsg(event.Event{Kind: event.Notice, Level: event.LevelInfo, Text: "line"}) cur := adv(newChatTUI(ctrl, "", make(chan event.Event, 1), 80), tea.WindowSizeMsg{Width: 80, Height: 12}) - for i := 0; i < 20; i++ { + for range 20 { cur = adv(cur, notice) } if !cur.shouldFollowTail() || !cur.viewport.AtBottom() { @@ -56,7 +56,7 @@ func TestUserScrollBreaksAndEmptyEnterRestoresFollow(t *testing.T) { notice := agentEventMsg(event.Event{Kind: event.Notice, Level: event.LevelInfo, Text: "line"}) cur := adv(newChatTUI(ctrl, "", make(chan event.Event, 1), 80), tea.WindowSizeMsg{Width: 80, Height: 10}) - for i := 0; i < 30; i++ { + for range 30 { cur = adv(cur, notice) } cur = adv(cur, tea.MouseWheelMsg{Button: tea.MouseWheelUp}) @@ -83,7 +83,7 @@ func TestScrollbarDragMotionSyncsBeforeRelease(t *testing.T) { } notice := agentEventMsg(event.Event{Kind: event.Notice, Level: event.LevelInfo, Text: "line"}) cur := adv(newChatTUI(ctrl, "", make(chan event.Event, 1), 80), tea.WindowSizeMsg{Width: 80, Height: 10}) - for i := 0; i < 40; i++ { + for range 40 { cur = adv(cur, notice) } if !cur.shouldFollowTail() { @@ -118,7 +118,7 @@ func TestWrapCacheAppendOnlyMatchesFullRebuild(t *testing.T) { m := newTestChatTUI() m.width = 40 cw := 40 - for i := 0; i < 5; i++ { + for i := range 5 { m.transcript = append(m.transcript, fmt.Sprintf("block-%d %s", i, strings.Repeat("word ", 20))) } m.rebuildWrappedLinesFull(cw) @@ -126,7 +126,7 @@ func TestWrapCacheAppendOnlyMatchesFullRebuild(t *testing.T) { m2 := newTestChatTUI() m2.width = 40 - for i := 0; i < 5; i++ { + for i := range 5 { m2.transcript = append(m2.transcript, m.transcript[i]) m2.syncWrappedLines(cw, i == 0) } @@ -150,7 +150,7 @@ func TestStreamAnswerSuffixInvalidationNotFullRebuild(t *testing.T) { } cur := adv(newChatTUI(ctrl, "", make(chan event.Event, 1), 80), tea.WindowSizeMsg{Width: 80, Height: 20}) // Seed history blocks. - for i := 0; i < 50; i++ { + for i := range 50 { cur = adv(cur, agentEventMsg(event.Event{ Kind: event.Notice, Level: event.LevelInfo, Text: fmt.Sprintf("hist-%d %s", i, strings.Repeat("x", 30)), @@ -279,14 +279,14 @@ func TestLongTranscriptStreamAnswerScalesSuffixOnly(t *testing.T) { } cur := adv(newChatTUI(ctrl, "", make(chan event.Event, 1), 80), tea.WindowSizeMsg{Width: 80, Height: 20}) const hist = 1000 - for i := 0; i < hist; i++ { + for i := range hist { cur = adv(cur, agentEventMsg(event.Event{ Kind: event.Notice, Level: event.LevelInfo, Text: fmt.Sprintf("h-%d-%s", i, strings.Repeat("y", 40)), })) } cur.state = tuiRunning - for i := 0; i < 30; i++ { + for i := range 30 { cur = adv(cur, agentEventMsg(event.Event{ Kind: event.Text, Text: fmt.Sprintf("stream paragraph %d with enough text to flush.\n\n", i), @@ -311,7 +311,7 @@ func BenchmarkStreamAnswerSuffixWrap(b *testing.B) { return n.(chatTUI) } base := adv(newChatTUI(ctrl, "", make(chan event.Event, 1), 80), tea.WindowSizeMsg{Width: 80, Height: 24}) - for i := 0; i < hist; i++ { + for i := range hist { base = adv(base, agentEventMsg(event.Event{ Kind: event.Notice, Level: event.LevelInfo, Text: fmt.Sprintf("h-%d-%s", i, strings.Repeat("z", 48)), @@ -319,7 +319,7 @@ func BenchmarkStreamAnswerSuffixWrap(b *testing.B) { } b.ReportAllocs() b.ResetTimer() - for i := 0; i < b.N; i++ { + for i := range b.N { m := base // copy model value (shallow; OK for bench of Update path) m.state = tuiRunning m.pending = &strings.Builder{} diff --git a/internal/cli/select.go b/internal/cli/select.go index 3eb5811a7c..f6d41098c7 100644 --- a/internal/cli/select.go +++ b/internal/cli/select.go @@ -42,10 +42,7 @@ func fixedLines(searching bool) int { // maxViewport calculates how many menu item rows fit after subtracting the // fixed lines from the available terminal rows, leaving at least 1 row. func maxViewport(totalItems, termRows int, searching bool) int { - avail := termRows - fixedLines(searching) - if avail < 1 { - avail = 1 - } + avail := max(termRows-fixedLines(searching), 1) if totalItems < avail { return totalItems } @@ -124,10 +121,7 @@ func selectOne(label string, items []menuItem) (int, error) { } // menu rows - end := scroll + vp - if end > n { - end = n - } + end := min(scroll+vp, n) for i := scroll; i < end; i++ { it := filtered[i] name := fmt.Sprintf("%-10s", it.name) @@ -308,10 +302,7 @@ func selectMany(label string, items []menuItem) ([]int, error) { fmt.Fprintf(w, "\r\033[K\r\n") } - end := scroll + vp - if end > n { - end = n - } + end := min(scroll+vp, n) for i := scroll; i < end; i++ { it := filtered[i] origIdx := filterIdx[i] diff --git a/internal/cli/select_test.go b/internal/cli/select_test.go index 77bdd0f1c3..02094a25f2 100644 --- a/internal/cli/select_test.go +++ b/internal/cli/select_test.go @@ -78,7 +78,7 @@ func TestFrameLinesNeverExceedsTerminal(t *testing.T) { // Otherwise the terminal scrolls and cursor repositioning drifts. termRows := 24 for _, searching := range []bool{false, true} { - for n := 0; n <= 200; n++ { + for n := range 201 { lines := FrameLines(n, termRows, searching) if lines > termRows { t.Errorf("FrameLines(%d, %d, searching=%v) = %d, exceeds terminal", diff --git a/internal/cli/setup_manager.go b/internal/cli/setup_manager.go index 91405ec015..8541e76b31 100644 --- a/internal/cli/setup_manager.go +++ b/internal/cli/setup_manager.go @@ -631,7 +631,7 @@ func promptOptionalAPIKeyEnvName(in *bufio.Scanner, w io.Writer, label, def stri func splitModels(raw string) []string { seen := map[string]bool{} var models []string - for _, model := range strings.Split(raw, ",") { + for model := range strings.SplitSeq(raw, ",") { model = strings.TrimSpace(model) if model != "" && !seen[model] { seen[model] = true diff --git a/internal/cli/shell_completion.go b/internal/cli/shell_completion.go index 0f4d84e79c..607812378b 100644 --- a/internal/cli/shell_completion.go +++ b/internal/cli/shell_completion.go @@ -3,6 +3,7 @@ package cli import ( "fmt" "os" + "slices" "strconv" "strings" @@ -448,10 +449,7 @@ func cliCompletionCandidatesWithValues(root cliCompletionSpec, cword int, words if cword < len(words) { current = words[cword] } - limit := cword - if limit > len(words) { - limit = len(words) - } + limit := min(cword, len(words)) ctx := &root positionalSeen := false @@ -537,10 +535,8 @@ func cliCompletionLookupFlag(spec *cliCompletionSpec, token string) (*cliComplet inline = true } for i := range spec.flags { - for _, candidate := range spec.flags[i].names { - if candidate == name { - return &spec.flags[i], inline - } + if slices.Contains(spec.flags[i].names, name) { + return &spec.flags[i], inline } } return nil, inline @@ -552,10 +548,8 @@ func cliCompletionLookupSubcommand(spec *cliCompletionSpec, token string) *cliCo if child.name == token { return child } - for _, alias := range child.aliases { - if alias == token { - return child - } + if slices.Contains(child.aliases, token) { + return child } } return nil diff --git a/internal/cli/shell_completion_test.go b/internal/cli/shell_completion_test.go index 9a35707603..4df004dffe 100644 --- a/internal/cli/shell_completion_test.go +++ b/internal/cli/shell_completion_test.go @@ -5,6 +5,7 @@ import ( "go/parser" "go/token" "reflect" + "slices" "strconv" "strings" "testing" @@ -296,12 +297,7 @@ func TestCLICompletionPathFlagReturnsEmptyForShellFallback(t *testing.T) { } func containsCompletionValue(values []string, target string) bool { - for _, value := range values { - if value == target { - return true - } - } - return false + return slices.Contains(values, target) } func completionRegistryKnowsRootToken(root *cliCompletionSpec, token string) bool { diff --git a/internal/cli/skill_hooks.go b/internal/cli/skill_hooks.go index 7ae6d34d63..5e76b31b14 100644 --- a/internal/cli/skill_hooks.go +++ b/internal/cli/skill_hooks.go @@ -4,6 +4,7 @@ import ( "fmt" "log/slog" "os" + "slices" "strings" tea "charm.land/bubbletea/v2" @@ -313,10 +314,5 @@ func (m *chatTUI) hooksList(cwd string) { } func containsArg(args []string, flag string) bool { - for _, a := range args { - if a == flag { - return true - } - } - return false + return slices.Contains(args, flag) } diff --git a/internal/cli/skill_picker_test.go b/internal/cli/skill_picker_test.go index 22e5eaac06..2f2fdce5ba 100644 --- a/internal/cli/skill_picker_test.go +++ b/internal/cli/skill_picker_test.go @@ -202,7 +202,7 @@ func TestSkillPickerRenderDialogStyle(t *testing.T) { t.Cleanup(func() { i18n.DetectLanguage("en") }) var skills []skill.Skill - for i := 0; i < 24; i++ { + for i := range 24 { skills = append(skills, skill.Skill{ Name: "skill-" + strings.Repeat("x", i%4) + string(rune('a'+i)), Description: "this long description should stay out of the default picker list", diff --git a/internal/cli/skill_picker_view.go b/internal/cli/skill_picker_view.go index c44ec5004c..2c279f486f 100644 --- a/internal/cli/skill_picker_view.go +++ b/internal/cli/skill_picker_view.go @@ -123,10 +123,7 @@ func skillListWindow(sel, total, limit int) (int, int) { if sel >= total { sel = total - 1 } - start := sel - limit/2 - if start < 0 { - start = 0 - } + start := max(sel-limit/2, 0) if start+limit > total { start = total - limit } diff --git a/internal/cli/slash_catalog_test.go b/internal/cli/slash_catalog_test.go index 7828c49f78..2393422cc9 100644 --- a/internal/cli/slash_catalog_test.go +++ b/internal/cli/slash_catalog_test.go @@ -18,7 +18,7 @@ func TestSlashCatalogCachesAcrossKeystrokes(t *testing.T) { ctrl := control.New(control.Options{}) m := newChatTUI(ctrl, "", make(chan event.Event, 1), 80) m.skills = make([]skill.Skill, 0, 50) - for i := 0; i < 50; i++ { + for i := range 50 { m.skills = append(m.skills, skill.Skill{ Name: fmt.Sprintf("skill-%03d", i), Description: strings.Repeat("description text for catalog build ", 20), @@ -221,7 +221,7 @@ func BenchmarkSlashCompletionKeystroke(b *testing.B) { ctrl := control.New(control.Options{}) m := newChatTUI(ctrl, "", make(chan event.Event, 1), 80) m.skills = make([]skill.Skill, 0, 1000) - for i := 0; i < 1000; i++ { + for i := range 1000 { m.skills = append(m.skills, skill.Skill{ Name: fmt.Sprintf("bench-skill-%04d", i), Description: "benchmark skill description " + strings.Repeat("x", 80), @@ -230,7 +230,7 @@ func BenchmarkSlashCompletionKeystroke(b *testing.B) { _ = m.slashItems() // warm catalog once b.ReportAllocs() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { m.input.SetValue("/be") m.updateCompletion() if !m.completion.active { diff --git a/internal/cli/slash_registry.go b/internal/cli/slash_registry.go index 88077fa254..f7e426abc1 100644 --- a/internal/cli/slash_registry.go +++ b/internal/cli/slash_registry.go @@ -1,6 +1,7 @@ package cli import ( + "slices" "strings" "reasonix/internal/i18n" @@ -93,10 +94,8 @@ func canonicalBuiltinSlashCommand(name string) string { if name == spec.name { return spec.name } - for _, alias := range spec.aliases { - if name == alias { - return spec.name - } + if slices.Contains(spec.aliases, name) { + return spec.name } } return name diff --git a/internal/cli/status_footer.go b/internal/cli/status_footer.go index 03a227db03..a97f6512d4 100644 --- a/internal/cli/status_footer.go +++ b/internal/cli/status_footer.go @@ -292,7 +292,7 @@ func (m chatTUI) renderStatusBlock(primary string, width int) string { // the optional shortcut help yields space to the composer. func hideStatusHintWhenKeyNamesCannotFit(primary string, width int) string { hint := i18n.M.ChatStatusCycleHintCompact - for _, group := range strings.Split(hint, " · ") { + for group := range strings.SplitSeq(hint, " · ") { if visibleWidth(statusFooterIndent+group) > width { return strings.Replace(primary, " · "+footerHint(hint), "", 1) } diff --git a/internal/cli/subagent.go b/internal/cli/subagent.go index 385fcb6bf1..0fde9aa185 100644 --- a/internal/cli/subagent.go +++ b/internal/cli/subagent.go @@ -558,7 +558,7 @@ func profileFlagsChanged(values subagentProfileFlags) bool { func parseToolList(raw string) []string { seen := map[string]bool{} var tools []string - for _, item := range strings.Split(raw, ",") { + for item := range strings.SplitSeq(raw, ",") { name := strings.TrimSpace(item) if name == "" || seen[name] { continue diff --git a/internal/cli/theme.go b/internal/cli/theme.go index 7604a3f3fc..5ba6d34ca9 100644 --- a/internal/cli/theme.go +++ b/internal/cli/theme.go @@ -256,11 +256,11 @@ func (c terminalRGB) looksLight() bool { } func parseOSC11Response(s string) (terminalRGB, bool) { - idx := strings.Index(s, "]11;") - if idx < 0 { + _, after, ok := strings.Cut(s, "]11;") + if !ok { return terminalRGB{}, false } - payload := s[idx+len("]11;"):] + payload := after if end := strings.IndexByte(payload, '\a'); end >= 0 { payload = payload[:end] } else if end := strings.Index(payload, "\x1b\\"); end >= 0 { @@ -272,8 +272,8 @@ func parseOSC11Response(s string) (terminalRGB, bool) { return terminalRGB{r, g, b}, ok } for _, prefix := range []string{"rgb:", "rgba:"} { - if strings.HasPrefix(payload, prefix) { - return parseOSCColorTriplet(strings.TrimPrefix(payload, prefix)) + if after, ok := strings.CutPrefix(payload, prefix); ok { + return parseOSCColorTriplet(after) } } return terminalRGB{}, false diff --git a/internal/cli/toolcard.go b/internal/cli/toolcard.go index 24251616d3..673bc8bd68 100644 --- a/internal/cli/toolcard.go +++ b/internal/cli/toolcard.go @@ -24,11 +24,12 @@ func connectorBlock(lines []string) string { return "" } indent := strings.Repeat(" ", len([]rune(connector))) - out := dim(connector) + lines[0] + var out strings.Builder + out.WriteString(dim(connector) + lines[0]) for _, ln := range lines[1:] { - out += "\n" + indent + ln + out.WriteString("\n" + indent + ln) } - return out + return out.String() } // toolVerb maps a tool's snake_case id to the verb shown in its card. diff --git a/internal/cli/transcript.go b/internal/cli/transcript.go index f6c193728c..a3bc768630 100644 --- a/internal/cli/transcript.go +++ b/internal/cli/transcript.go @@ -317,7 +317,7 @@ func (a transcriptResizeAnchor) yOffset(blocks []string, width int) int { } block := min(max(a.block, 0), len(blocks)-1) offset := 0 - for i := 0; i < block; i++ { + for i := range block { offset += transcriptBlockLineCount(blocks[i], width) } lines := transcriptBlockLineCount(blocks[block], width) @@ -473,7 +473,7 @@ func (m chatTUI) renderTranscript() string { rows := make([]string, h) bar := make([]string, h) - for r := 0; r < h; r++ { + for r := range h { idx := yoff + r line := blank // off-content rows fill to width if idx >= 0 && idx < total { @@ -717,15 +717,9 @@ func scrollbarThumb(height, yoff, total int) (start, size int) { if total <= height { return 0, 0 // no overflow → no thumb } - size = height * height / total - if size < 1 { - size = 1 - } + size = max(height*height/total, 1) maxYoff := total - height - start = yoff * (height - size) / maxYoff - if start > height-size { - start = height - size - } + start = min(yoff*(height-size)/maxYoff, height-size) return start, size } @@ -738,13 +732,7 @@ func scrollbarYOffset(height, row, total, grabOffset int) int { if maxTop <= 0 { return 0 } - top := row - grabOffset - if top < 0 { - top = 0 - } - if top > maxTop { - top = maxTop - } + top := min(max(row-grabOffset, 0), maxTop) maxYoff := total - height return (top*maxYoff + maxTop/2) / maxTop } diff --git a/internal/cli/transcript_test.go b/internal/cli/transcript_test.go index aba8d71d3c..8ff0feed9f 100644 --- a/internal/cli/transcript_test.go +++ b/internal/cli/transcript_test.go @@ -166,13 +166,13 @@ func TestSelectedTextRestoresMathWithoutReusingRawColumns(t *testing.T) { } plain := ansi.Strip(m.wrappedLines[lineIndex]) - formulaByte := strings.Index(plain, "α") - afterByte := strings.Index(plain, "after") - if formulaByte < 0 || afterByte < 0 { + before, _, ok := strings.Cut(plain, "α") + before0, _, ok0 := strings.Cut(plain, "after") + if !ok || !ok0 { t.Fatalf("math line = %q", plain) } - formulaCol := ansi.StringWidth(plain[:formulaByte]) - afterCol := ansi.StringWidth(plain[:afterByte]) + formulaCol := ansi.StringWidth(before) + afterCol := ansi.StringWidth(before0) m.sel = selection{ active: true, @@ -218,12 +218,12 @@ func TestSelectedTextRestoresMathFromReplayBundle(t *testing.T) { formulaCol := -1 for i, line := range m.wrappedLines { plain := ansi.Strip(line) - formulaByte := strings.Index(plain, "α") - if formulaByte < 0 { + before, _, ok := strings.Cut(plain, "α") + if !ok { continue } lineIndex = i - formulaCol = ansi.StringWidth(plain[:formulaByte]) + formulaCol = ansi.StringWidth(before) break } if lineIndex < 0 { @@ -283,12 +283,12 @@ func TestSelectedTextPreservesProseAroundMath(t *testing.T) { for i, line := range m.wrappedLines { plain := ansi.Strip(line) - startByte := strings.Index(plain, "before") + before, _, ok := strings.Cut(plain, "before") endByte := strings.Index(plain, " after") - if startByte < 0 || endByte < 0 { + if !ok || endByte < 0 { continue } - startCol := ansi.StringWidth(plain[:startByte]) + startCol := ansi.StringWidth(before) endCol := ansi.StringWidth(plain[:endByte+len(" after")]) m.sel = selection{ active: true, diff --git a/internal/cli/tui_diagnostics.go b/internal/cli/tui_diagnostics.go index 46de20147f..53f7e89a12 100644 --- a/internal/cli/tui_diagnostics.go +++ b/internal/cli/tui_diagnostics.go @@ -290,11 +290,9 @@ func (d *tuiDiagnostics) StartWatchdog(p *tea.Program) { d.lastHeartbeatSource = "watchdog_armed" } d.mu.Unlock() - d.watchWG.Add(1) - go func() { - defer d.watchWG.Done() + d.watchWG.Go(func() { d.watch() - }() + }) }) } diff --git a/internal/cli/tui_diagnostics_test.go b/internal/cli/tui_diagnostics_test.go index 03ed423e0d..96a8a7a52e 100644 --- a/internal/cli/tui_diagnostics_test.go +++ b/internal/cli/tui_diagnostics_test.go @@ -200,7 +200,7 @@ func TestWatchdogIdleNeverEscalates(t *testing.T) { t.Fatalf("phase = %s, want idle", d.phaseForTest()) } // Idle for well over the stall threshold. - for i := 0; i < 30; i++ { + for range 30 { clock.now = clock.now.Add(time.Second) d.onTick(clock.now) } @@ -244,7 +244,7 @@ func TestWatchdogRunningElapsedHeartbeatPreventsKill(t *testing.T) { d.NoteBooted() d.NoteRunning(func() {}) // Simulate a long turn with a heartbeat every second. - for i := 0; i < 60; i++ { + for range 60 { clock.now = clock.now.Add(time.Second) d.NoteActiveHeartbeat("elapsed_tick") d.onTick(clock.now) diff --git a/internal/cli/upgrade.go b/internal/cli/upgrade.go index 74dfab729a..35ab7a1323 100644 --- a/internal/cli/upgrade.go +++ b/internal/cli/upgrade.go @@ -529,7 +529,7 @@ func fetchLatestRelease(c *http.Client, channel cliReleaseChannel) (*ghRelease, return pointerRelease, nil } - req, err := http.NewRequest("GET", ghAPIReleases, nil) + req, err := http.NewRequest(http.MethodGet, ghAPIReleases, nil) if err != nil { return nil, err } @@ -560,7 +560,7 @@ func fetchLatestRelease(c *http.Client, channel cliReleaseChannel) (*ghRelease, } func fetchCLIReleasePointer(c *http.Client, pointerURL string, channel cliReleaseChannel) (*ghRelease, error) { - req, err := http.NewRequest("GET", pointerURL, nil) + req, err := http.NewRequest(http.MethodGet, pointerURL, nil) if err != nil { return nil, err } @@ -617,7 +617,7 @@ func verifyChecksum(data []byte, fileName string, checksumFile []byte) error { sum := sha256.Sum256(data) got := hex.EncodeToString(sum[:]) - for _, line := range strings.Split(strings.TrimSpace(string(checksumFile)), "\n") { + for line := range strings.SplitSeq(strings.TrimSpace(string(checksumFile)), "\n") { line = strings.TrimSpace(line) if line == "" { continue diff --git a/internal/cli/upgrade_test.go b/internal/cli/upgrade_test.go index 9e6f960f3b..963748e949 100644 --- a/internal/cli/upgrade_test.go +++ b/internal/cli/upgrade_test.go @@ -48,21 +48,21 @@ func TestVerifyChecksum(t *testing.T) { hash := hex.EncodeToString(sum[:]) t.Run("match", func(t *testing.T) { - checksumFile := []byte(fmt.Sprintf("%s reasonix-linux-amd64.tar.gz\n", hash)) + checksumFile := fmt.Appendf(nil, "%s reasonix-linux-amd64.tar.gz\n", hash) if err := verifyChecksum(content, "reasonix-linux-amd64.tar.gz", checksumFile); err != nil { t.Errorf("unexpected error: %v", err) } }) t.Run("mismatch", func(t *testing.T) { - checksumFile := []byte(fmt.Sprintf("%s reasonix-linux-amd64.tar.gz\n", "0000000000000000000000000000000000000000000000000000000000000000")) + checksumFile := fmt.Appendf(nil, "%s reasonix-linux-amd64.tar.gz\n", "0000000000000000000000000000000000000000000000000000000000000000") if err := verifyChecksum(content, "reasonix-linux-amd64.tar.gz", checksumFile); err == nil { t.Error("expected checksum mismatch error") } }) t.Run("not found", func(t *testing.T) { - checksumFile := []byte(fmt.Sprintf("%s reasonix-darwin-arm64.tar.gz\n", hash)) + checksumFile := fmt.Appendf(nil, "%s reasonix-darwin-arm64.tar.gz\n", hash) if err := verifyChecksum(content, "reasonix-linux-amd64.tar.gz", checksumFile); err == nil { t.Error("expected not-found error") } diff --git a/internal/cli/width_test.go b/internal/cli/width_test.go index f93882082e..de77e3cba6 100644 --- a/internal/cli/width_test.go +++ b/internal/cli/width_test.go @@ -37,7 +37,7 @@ func TestVisibleWidthGraphemeClusters(t *testing.T) { // preserves ANSI escapes as zero width, and leaves in-width lines untouched. func TestClampWidthHardwrap(t *testing.T) { // CJK hard-breaks at the column boundary (each is 2 cols); no line over width. - for _, line := range strings.Split(clampWidth("中文字", 4), "\n") { + for line := range strings.SplitSeq(clampWidth("中文字", 4), "\n") { if visibleWidth(line) > 4 { t.Errorf("cjk line %q exceeds width 4 (got %d)", line, visibleWidth(line)) } @@ -55,7 +55,7 @@ func TestClampWidthHardwrap(t *testing.T) { } // Every wrapped line stays within the column budget. - for _, line := range strings.Split(clampWidth(strings.Repeat("中", 10), 6), "\n") { + for line := range strings.SplitSeq(clampWidth(strings.Repeat("中", 10), 6), "\n") { if visibleWidth(line) > 6 { t.Errorf("line %q exceeds width 6 (got %d)", line, visibleWidth(line)) } diff --git a/internal/cli/wrap_cache.go b/internal/cli/wrap_cache.go index 5f66d3d744..a6a2e9f05b 100644 --- a/internal/cli/wrap_cache.go +++ b/internal/cli/wrap_cache.go @@ -63,7 +63,7 @@ func (m *chatTUI) rebuildWrappedLinesFull(contentW int) bool { } n := len(m.transcript) m.wrapBlockLines = make([][]string, n) - for i := 0; i < n; i++ { + for i := range n { m.wrapBlockLines[i] = wrapBlockLines(m.transcript[i], contentW) } // Prefer per-block flatten over join-then-wrap so streaming suffix rebuilds diff --git a/internal/command/inspect.go b/internal/command/inspect.go index 5da316b6fe..430b82bc1e 100644 --- a/internal/command/inspect.go +++ b/internal/command/inspect.go @@ -3,6 +3,7 @@ package command import ( "os" "path/filepath" + "slices" "sort" "strings" ) @@ -89,8 +90,8 @@ func Inspect(dirs ...string) Inspection { for name, list := range byName { // Find last non-error candidate as winner; errors stay as errors. winIdx := -1 - for i := len(list) - 1; i >= 0; i-- { - if list[i].Status != CandidateError { + for i, v := range slices.Backward(list) { + if v.Status != CandidateError { winIdx = i break } diff --git a/internal/command/slashtool_test.go b/internal/command/slashtool_test.go index bbeecb7e64..d4a2d81b7a 100644 --- a/internal/command/slashtool_test.go +++ b/internal/command/slashtool_test.go @@ -144,7 +144,7 @@ func TestPluginSlashToolShowsOnlyCanonicalQualifiedName(t *testing.T) { if cmd.Hidden { continue } - cmd := cmd + out = append(out, SlashEntry{Name: cmd.Name, Description: cmd.Description, ArgHint: cmd.ArgHint, Render: func(args []string) string { return cmd.Render(args) }}) } return out diff --git a/internal/config/backfill_test.go b/internal/config/backfill_test.go index e2d4f2aa86..79977bed6f 100644 --- a/internal/config/backfill_test.go +++ b/internal/config/backfill_test.go @@ -4,6 +4,7 @@ import ( "os" "path/filepath" "reflect" + "slices" "strings" "testing" @@ -14,10 +15,8 @@ import ( func hasModel(c *Config, model string) *ProviderEntry { for i := range c.Providers { - for _, m := range c.Providers[i].ModelList() { - if m == model { - return &c.Providers[i] - } + if slices.Contains(c.Providers[i].ModelList(), model) { + return &c.Providers[i] } } return nil @@ -572,7 +571,7 @@ func TestNormalizeLegacyKimiK3CatalogMigratesOnlyOfficialUntouchedPresets(t *tes if !normalizeLegacyKimiK3Catalog(c) { t.Fatal("legacy Kimi direct catalogs did not report a change") } - for i := 0; i < 2; i++ { + for i := range 2 { got := &c.Providers[i] if !got.HasModel("kimi-k3") || !got.HasVisionModel("kimi-k3") { t.Fatalf("migrated Kimi provider %d = %+v, want K3 with vision", i, got) diff --git a/internal/config/config.go b/internal/config/config.go index 35d4e0c757..93df15988d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -15,6 +15,7 @@ import ( "path/filepath" "regexp" "runtime" + "slices" "strings" fileencoding "reasonix/internal/fileutil/encoding" @@ -1492,12 +1493,7 @@ func (e *ProviderEntry) DefaultModel() string { // HasModel reports whether m is one of the provider's models. func (e *ProviderEntry) HasModel(m string) bool { - for _, x := range e.ModelList() { - if x == m { - return true - } - } - return false + return slices.Contains(e.ModelList(), m) } // PriceForModel returns the configured per-1M-token price for model. Per-model diff --git a/internal/config/credentials_keyring_unix.go b/internal/config/credentials_keyring_unix.go index afe5bbf7b0..df55b68d34 100644 --- a/internal/config/credentials_keyring_unix.go +++ b/internal/config/credentials_keyring_unix.go @@ -5,6 +5,7 @@ package config import ( "context" "fmt" + "slices" "strings" dbus "github.com/godbus/dbus/v5" @@ -185,10 +186,8 @@ func ssResolveLoginCollection(ctx context.Context, svc dbus.BusObject) (dbus.Obj return dbus.ObjectPath(ssLoginAlias), nil } paths, _ := val.Value().([]dbus.ObjectPath) - for _, p := range paths { - if p == path { - return path, nil - } + if slices.Contains(paths, path) { + return path, nil } return dbus.ObjectPath(ssLoginAlias), nil } diff --git a/internal/config/dotenv.go b/internal/config/dotenv.go index c482697e0f..ec4b3c2851 100644 --- a/internal/config/dotenv.go +++ b/internal/config/dotenv.go @@ -172,7 +172,7 @@ func detectDotEnvDuplicateKeys(path string) []string { } seen := map[string]bool{} dups := map[string]bool{} - for _, line := range strings.Split(strings.ReplaceAll(string(raw), "\r\n", "\n"), "\n") { + for line := range strings.SplitSeq(strings.ReplaceAll(string(raw), "\r\n", "\n"), "\n") { values, err := godotenv.Unmarshal(line) if err != nil { continue diff --git a/internal/config/edit.go b/internal/config/edit.go index 8e0ef5c472..bef515c9c3 100644 --- a/internal/config/edit.go +++ b/internal/config/edit.go @@ -676,10 +676,8 @@ func (c *Config) AddPermissionRule(list, rule string) error { if _, ok := permission.ParseRule(rule); !ok { return fmt.Errorf("invalid permission rule %q (want \"ToolName\" or \"ToolName(glob)\")", rule) } - for _, existing := range *target { - if existing == rule { - return nil // already present - } + if slices.Contains(*target, rule) { + return nil // already present } *target = append(*target, rule) return nil @@ -1704,7 +1702,7 @@ func mergeTOMLDelta(body, delta string) string { } func mergeTOMLTopLevelFields(body, fields string) string { - for _, line := range strings.Split(fields, "\n") { + for line := range strings.SplitSeq(fields, "\n") { line = strings.TrimSpace(line) if line == "" { continue diff --git a/internal/config/effort.go b/internal/config/effort.go index dd6b31a3e8..006c943bcd 100644 --- a/internal/config/effort.go +++ b/internal/config/effort.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "slices" "strings" "reasonix/internal/provider/openai" @@ -477,12 +478,7 @@ func effortNotConfigurableError(e *ProviderEntry) error { } func containsString(haystack []string, needle string) bool { - for _, s := range haystack { - if s == needle { - return true - } - } - return false + return slices.Contains(haystack, needle) } func normalizeEffortLevel(s string) string { diff --git a/internal/config/fetch.go b/internal/config/fetch.go index f1befa1661..51ccbd318d 100644 --- a/internal/config/fetch.go +++ b/internal/config/fetch.go @@ -4,6 +4,7 @@ package config import ( "context" "fmt" + "slices" "strings" "reasonix/internal/provider/openai" @@ -125,13 +126,7 @@ func uniqueStrings(in []string) []string { if s == "" { continue } - seen := false - for _, existing := range out { - if existing == s { - seen = true - break - } - } + seen := slices.Contains(out, s) if !seen { out = append(out, s) } diff --git a/internal/config/load.go b/internal/config/load.go index db3ff02d46..90428257f0 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -3,9 +3,11 @@ package config import ( "fmt" "log/slog" + "maps" "net/url" "os" "path/filepath" + "slices" "strings" "github.com/BurntSushi/toml" @@ -278,9 +280,7 @@ func cloneStringMap(in map[string]string) map[string]string { return nil } out := make(map[string]string, len(in)) - for k, v := range in { - out[k] = v - } + maps.Copy(out, in) return out } @@ -1946,15 +1946,11 @@ func mergeModelLists(primary, extra []string) []string { func firstKnownModel(current string, models []string, fallback string) string { current = strings.TrimSpace(current) - for _, model := range models { - if model == current { - return current - } + if slices.Contains(models, current) { + return current } - for _, model := range models { - if model == fallback { - return fallback - } + if slices.Contains(models, fallback) { + return fallback } if len(models) > 0 { return models[0] diff --git a/internal/config/mcp_activation_test.go b/internal/config/mcp_activation_test.go index 41a5485f42..c25c56104f 100644 --- a/internal/config/mcp_activation_test.go +++ b/internal/config/mcp_activation_test.go @@ -88,9 +88,7 @@ func TestMCPActivationStoreConcurrentIndependentWriters(t *testing.T) { start := make(chan struct{}) var wg sync.WaitGroup for i, store := range stores { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { <-start if err := store.SetEnabled(MCPActivationOverride{ Scope: MCPActivationGlobal, @@ -99,7 +97,7 @@ func TestMCPActivationStoreConcurrentIndependentWriters(t *testing.T) { }); err != nil { t.Errorf("SetEnabled(%d): %v", i, err) } - }() + }) } close(start) wg.Wait() diff --git a/internal/config/migrate.go b/internal/config/migrate.go index eee6f146b8..ea55f0cfea 100644 --- a/internal/config/migrate.go +++ b/internal/config/migrate.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "maps" "os" "path/filepath" "sort" @@ -670,12 +671,8 @@ func mergeEnv(base, overlay map[string]string) map[string]string { return nil } out := make(map[string]string, len(base)+len(overlay)) - for k, v := range base { - out[k] = v - } - for k, v := range overlay { - out[k] = v - } + maps.Copy(out, base) + maps.Copy(out, overlay) return out } diff --git a/internal/config/mutate.go b/internal/config/mutate.go index 4d1ca92005..5e9dd012e2 100644 --- a/internal/config/mutate.go +++ b/internal/config/mutate.go @@ -8,6 +8,7 @@ import ( osuser "os/user" "path/filepath" "runtime" + "slices" "sort" "strings" "sync" @@ -164,16 +165,16 @@ func lockConfigFilesEdits(paths ...string) (func(), error) { unlockFiles := make([]func(), 0, len(lockPaths)) for _, lockPath := range lockPaths { if err := os.MkdirAll(filepath.Dir(lockPath), 0o700); err != nil { - for i := len(unlockFiles) - 1; i >= 0; i-- { - unlockFiles[i]() + for _, v := range slices.Backward(unlockFiles) { + v() } userEditMu.Unlock() return nil, fmt.Errorf("lock config edits: create lock directory: %w", err) } unlockFile, err := acquireConfigEditLockPath(ctx, lockPath) if err != nil { - for i := len(unlockFiles) - 1; i >= 0; i-- { - unlockFiles[i]() + for _, v := range slices.Backward(unlockFiles) { + v() } userEditMu.Unlock() return nil, fmt.Errorf("lock config edits: %w", err) @@ -186,8 +187,8 @@ func lockConfigFilesEdits(paths ...string) (func(), error) { return func() { once.Do(func() { clearPins() - for i := len(unlockFiles) - 1; i >= 0; i-- { - unlockFiles[i]() + for _, v := range slices.Backward(unlockFiles) { + v() } userEditMu.Unlock() }) diff --git a/internal/config/mutate_test.go b/internal/config/mutate_test.go index a19974c7b3..70d51a6893 100644 --- a/internal/config/mutate_test.go +++ b/internal/config/mutate_test.go @@ -31,7 +31,7 @@ func TestLockUserConfigEditsSerializesRMW(t *testing.T) { const writers = 8 var wg sync.WaitGroup - for i := 0; i < writers; i++ { + for i := range writers { wg.Add(1) go func(n int) { defer wg.Done() diff --git a/internal/config/path_access.go b/internal/config/path_access.go index 8688beaea5..67c825e5ec 100644 --- a/internal/config/path_access.go +++ b/internal/config/path_access.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "strings" ) @@ -65,8 +66,8 @@ func evalSymlinksAllowMissing(path string) (string, error) { if err != nil { return "", err } - for i := len(suffix) - 1; i >= 0; i-- { - resolved = filepath.Join(resolved, suffix[i]) + for _, v := range slices.Backward(suffix) { + resolved = filepath.Join(resolved, v) } return filepath.Clean(resolved), nil } else if !os.IsNotExist(err) { diff --git a/internal/config/paths.go b/internal/config/paths.go index bf09026d2d..c680f856fd 100644 --- a/internal/config/paths.go +++ b/internal/config/paths.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "runtime" + "slices" "strings" "unicode/utf8" @@ -540,8 +541,8 @@ var ConventionDirs = []string{".reasonix", ".agents", ".agent", ".claude"} // highest-priority entry — command.Load lets a later directory win on a clash. func conventionSubdirsAsc(base, sub string) []string { out := make([]string, 0, len(ConventionDirs)) - for i := len(ConventionDirs) - 1; i >= 0; i-- { - out = append(out, filepath.Join(base, ConventionDirs[i], sub)) + for _, v := range slices.Backward(ConventionDirs) { + out = append(out, filepath.Join(base, v, sub)) } return out } diff --git a/internal/config/remote_credentials.go b/internal/config/remote_credentials.go index ec956de7bb..b89cf10932 100644 --- a/internal/config/remote_credentials.go +++ b/internal/config/remote_credentials.go @@ -3,6 +3,7 @@ package config import ( "crypto/sha256" "fmt" + "slices" "strings" ) @@ -96,8 +97,8 @@ func EditUserConfigWithCredentials(mutate func(*Config) ([]CredentialChange, err applied := make([]string, 0, len(changes)) rollback := func() { seen := map[string]bool{} - for i := len(applied) - 1; i >= 0; i-- { - key := applied[i] + for _, v := range slices.Backward(applied) { + key := v if seen[key] { continue } diff --git a/internal/config/render_test.go b/internal/config/render_test.go index 0d9a9fa585..0dcd2d3daa 100644 --- a/internal/config/render_test.go +++ b/internal/config/render_test.go @@ -736,7 +736,7 @@ extensions = [".cc", ".cpp", ".hpp"] func BenchmarkRenderTOMLWithLSPServers(b *testing.B) { cfg := Default() cfg.LSP.Servers = make(map[string]LSPServer, 64) - for i := 0; i < 64; i++ { + for i := range 64 { lang := "lang" + strconv.Itoa(i) cfg.LSP.Servers[lang] = LSPServer{ Command: "server-" + strconv.Itoa(i), @@ -749,7 +749,7 @@ func BenchmarkRenderTOMLWithLSPServers(b *testing.B) { } b.ReportAllocs() - for i := 0; i < b.N; i++ { + for range b.N { rendered := RenderTOML(cfg) if len(rendered) == 0 { b.Fatal("empty render") @@ -1304,7 +1304,7 @@ func TestRenderTOMLWindowsSandboxDefaultAndExplicitEnforceDisabled(t *testing.T) func extractSectionLines(toml, section string) []string { var lines []string inSection := false - for _, line := range strings.Split(toml, "\n") { + for line := range strings.SplitSeq(toml, "\n") { trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, section) { inSection = true diff --git a/internal/config/system_prompt_test.go b/internal/config/system_prompt_test.go index 207d083daf..780bd58dfa 100644 --- a/internal/config/system_prompt_test.go +++ b/internal/config/system_prompt_test.go @@ -246,7 +246,7 @@ func TestResolveSystemPromptRejectsProjectPathEscapes(t *testing.T) { t.Run(test.name, func(t *testing.T) { root := t.TempDir() outside := test.path(root) - if err := os.WriteFile(filepath.Join(root, "reasonix.toml"), []byte(fmt.Sprintf("[agent]\nsystem_prompt_file = %q\n", outside)), 0o600); err != nil { + if err := os.WriteFile(filepath.Join(root, "reasonix.toml"), fmt.Appendf(nil, "[agent]\nsystem_prompt_file = %q\n", outside), 0o600); err != nil { t.Fatal(err) } cfg, err := LoadForRootReadOnly(root) diff --git a/internal/config/vision.go b/internal/config/vision.go index 5a32fdad92..438b7e6424 100644 --- a/internal/config/vision.go +++ b/internal/config/vision.go @@ -2,6 +2,7 @@ package config import ( "net/url" + "slices" "strings" "reasonix/internal/provider/openai" @@ -39,10 +40,8 @@ func IsLikelyVisionModel(model string) bool { return true } tokens := strings.FieldsFunc(lower, modelTokenSeparator) - for _, token := range tokens { - if token == "audio" { - return false - } + if slices.Contains(tokens, "audio") { + return false } if strings.HasPrefix(lower, "gpt-4o") { return true diff --git a/internal/control/admission_test.go b/internal/control/admission_test.go index 37f0f275f9..4146522a39 100644 --- a/internal/control/admission_test.go +++ b/internal/control/admission_test.go @@ -16,9 +16,9 @@ import ( // deterministically open so tests can place submits inside it. Later // TurnDones pass through unblocked. func holdFinishingWindow(release <-chan struct{}, entered chan<- struct{}, events chan<- event.Event) event.Sink { - var first int32 + var first atomic.Int32 return event.FuncSink(func(e event.Event) { - if e.Kind == event.TurnDone && atomic.AddInt32(&first, 1) == 1 { + if e.Kind == event.TurnDone && first.Add(1) == 1 { entered <- struct{}{} <-release } @@ -44,7 +44,7 @@ func TestParkedTurnsRunFIFO(t *testing.T) { var order []int ran := make(chan int, 3) for i := 1; i <= 3; i++ { - i := i + if got := c.runGuarded(func(context.Context) error { ran <- i return nil @@ -117,10 +117,10 @@ func TestSubmitDuringRotationEmitsNotice(t *testing.T) { // opportunistic callers rely on the quiet no-op. func TestSubmitWhileRunningStaysSilentNoOp(t *testing.T) { block := make(chan struct{}) - var notices int32 + var notices atomic.Int32 c := New(Options{Sink: event.FuncSink(func(e event.Event) { if e.Kind == event.Notice { - atomic.AddInt32(¬ices, 1) + notices.Add(1) } })}) @@ -135,7 +135,7 @@ func TestSubmitWhileRunningStaysSilentNoOp(t *testing.T) { if got := c.runGuarded(func(context.Context) error { return nil }); got != turnDroppedRunning { t.Fatalf("admission while running = %v, want turnDroppedRunning", got) } - if n := atomic.LoadInt32(¬ices); n != 0 { + if n := notices.Load(); n != 0 { t.Fatalf("running drop should stay silent, got %d notices", n) } close(block) diff --git a/internal/control/attachments.go b/internal/control/attachments.go index 4fbea1eff0..718036b37f 100644 --- a/internal/control/attachments.go +++ b/internal/control/attachments.go @@ -33,11 +33,11 @@ var safeAttachmentExt = regexp.MustCompile(`^\.[a-z0-9]{1,12}$`) // origName supplies only the extension; the stored name is generated. func SaveAttachmentDataURL(origName, dataURL string) (string, error) { const marker = ";base64," - i := strings.Index(dataURL, marker) - if !strings.HasPrefix(dataURL, "data:") || i < 0 { + _, after, ok := strings.Cut(dataURL, marker) + if !strings.HasPrefix(dataURL, "data:") || !ok { return "", fmt.Errorf("unsupported pasted file") } - raw, err := base64.StdEncoding.DecodeString(dataURL[i+len(marker):]) + raw, err := base64.StdEncoding.DecodeString(after) if err != nil { return "", fmt.Errorf("decode pasted file: %w", err) } @@ -373,7 +373,7 @@ func rejectSymlinkComponents(path, root string) error { return fmt.Errorf("attachment path is outside .reasonix/attachments") } cur := root - for _, part := range strings.Split(rel, string(filepath.Separator)) { + for part := range strings.SplitSeq(rel, string(filepath.Separator)) { if part == "" || part == "." { continue } diff --git a/internal/control/autoresearch_manager.go b/internal/control/autoresearch_manager.go index 6ea5d10427..fd84afbc1a 100644 --- a/internal/control/autoresearch_manager.go +++ b/internal/control/autoresearch_manager.go @@ -323,7 +323,7 @@ func parseAutoResearchEvidenceBlocks(text string) []autoResearchEvidenceBlock { func autoResearchDirectionSummary(text string) string { text = agent.StripAutoResearchEvidenceBlocks(text) - for _, line := range strings.Split(text, "\n") { + for line := range strings.SplitSeq(text, "\n") { line = strings.TrimSpace(line) lower := strings.ToLower(line) if line == "" || strings.HasPrefix(lower, "[goal:") { diff --git a/internal/control/branches.go b/internal/control/branches.go index 2eb8d25660..7f9cd65a88 100644 --- a/internal/control/branches.go +++ b/internal/control/branches.go @@ -99,10 +99,7 @@ func branchTitle(b agent.BranchInfo, depth int) string { if label, ok := structuredBranchLabel(title); ok { return label } - maxRunes := 32 - depth*4 - if maxRunes < 18 { - maxRunes = 18 - } + maxRunes := max(32-depth*4, 18) title = oneLineBranch(title, maxRunes) if title == "" { return "(untitled)" diff --git a/internal/control/controller.go b/internal/control/controller.go index 5836c8228c..46510b1fb7 100644 --- a/internal/control/controller.go +++ b/internal/control/controller.go @@ -19,6 +19,7 @@ import ( "net/http" "os" "path/filepath" + "slices" "sort" "strconv" "strings" @@ -924,11 +925,9 @@ func (c *Controller) admitGuardedTurn(body func(ctx context.Context) error, park // spawnGuardedTurn launches an admitted turn body plus its autosave companion. // The caller must already have claimed admission (running=true) under c.mu. func (c *Controller) spawnGuardedTurn(ctx context.Context, cancel context.CancelFunc, body func(ctx context.Context) error) { - c.autosaveWG.Add(1) - go func() { - defer c.autosaveWG.Done() + c.autosaveWG.Go(func() { c.autosaveWhileRunning(ctx) - }() + }) go func() { defer cancel() defer func() { @@ -1149,9 +1148,9 @@ func (c *Controller) stopGoal(status string) { // lastAssistantText returns the content of the most recent assistant message with // non-empty text — the model's final answer for the turn (its plan, in plan mode). func lastAssistantText(msgs []provider.Message) string { - for i := len(msgs) - 1; i >= 0; i-- { - if msgs[i].Role == provider.RoleAssistant && strings.TrimSpace(msgs[i].Content) != "" { - return msgs[i].Content + for _, msg := range slices.Backward(msgs) { + if msg.Role == provider.RoleAssistant && strings.TrimSpace(msg.Content) != "" { + return msg.Content } } return "" @@ -1626,14 +1625,7 @@ func (c *Controller) applyPlanExec(input, display string) { } // Parse --strict flag. - strict := false - fields := strings.Fields(input) - for _, f := range fields { - if f == "--strict" { - strict = true - break - } - } + strict := slices.Contains(strings.Fields(input), "--strict") // Count completion status. total := len(todos) @@ -4424,8 +4416,8 @@ func resolveInterruptedTurnStart(msgs []provider.Message, idx int, preserveUser // graceful fallback still distinguishes the current visible turn; search // backward so a repeated prompt selects the newest occurrence. if fallbackContent != "" { - for i := len(msgs) - 1; i >= 0; i-- { - if matchesKind(msgs[i]) { + for i, msg := range slices.Backward(msgs) { + if matchesKind(msg) { return i, true } } @@ -4498,10 +4490,8 @@ func appendUniqueString(dst []string, value string) []string { if value == "" { return dst } - for _, existing := range dst { - if existing == value { - return dst - } + if slices.Contains(dst, value) { + return dst } return append(dst, value) } @@ -4515,10 +4505,8 @@ func interruptedToolSummary(call provider.ToolCall) provider.InterruptedToolSumm if path == "" || path == "/dev/null" || len(summary.Files) >= 8 { return } - for _, existing := range summary.Files { - if existing == path { - return - } + if slices.Contains(summary.Files, path) { + return } summary.Files = append(summary.Files, path) } @@ -4530,7 +4518,7 @@ func interruptedToolSummary(call provider.ToolCall) provider.InterruptedToolSumm } } } - for _, line := range strings.Split(call.Diff, "\n") { + for line := range strings.SplitSeq(call.Diff, "\n") { line = strings.TrimSpace(line) switch { case strings.HasPrefix(line, "+++ b/"): @@ -4780,14 +4768,14 @@ func (c *Controller) ToolResult(toolID string) *ToolResultData { msgs := c.executor.Session().Snapshot() // Search backwards: tool result first (most recent), then find the args // from the preceding assistant turn. - for i := len(msgs) - 1; i >= 0; i-- { - if msgs[i].Role != provider.RoleTool || msgs[i].ToolCallID != toolID { + for i, msg := range slices.Backward(msgs) { + if msg.Role != provider.RoleTool || msg.ToolCallID != toolID { continue } out := &ToolResultData{ Args: "", - Output: msgs[i].Content, - Execution: msgs[i].ToolExecution, + Output: msg.Content, + Execution: msg.ToolExecution, } // Walk back to find the assistant turn that issued this call. for j := i; j >= 0; j-- { @@ -4843,7 +4831,7 @@ func (c *Controller) ReloadCommands(ctx context.Context) error { entries := make([]command.SlashEntry, 0, len(cmdSkills)+len(cmds)) for _, sk := range cmdSkills { - sk := sk + entries = append(entries, command.SlashEntry{ Name: sk.SlashName(), Description: sk.Description, @@ -4854,7 +4842,7 @@ func (c *Controller) ReloadCommands(ctx context.Context) error { if cmd.Hidden { continue } - cmd := cmd + entries = append(entries, command.SlashEntry{ Name: cmd.Name, Description: cmd.Description, diff --git a/internal/control/controller_lock_test.go b/internal/control/controller_lock_test.go index 2b47fe4658..8e233d191b 100644 --- a/internal/control/controller_lock_test.go +++ b/internal/control/controller_lock_test.go @@ -52,10 +52,8 @@ func TestRewindConcurrentWithHistoryReads(t *testing.T) { stop := make(chan struct{}) var wg sync.WaitGroup - for i := 0; i < 4; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range 4 { + wg.Go(func() { for { select { case <-stop: @@ -65,7 +63,7 @@ func TestRewindConcurrentWithHistoryReads(t *testing.T) { _ = c.CheckpointHasBoundary(lastTurn) } } - }() + }) } err := c.Rewind(lastTurn, RewindConversation) diff --git a/internal/control/controller_test.go b/internal/control/controller_test.go index 59d8a75188..8782312f15 100644 --- a/internal/control/controller_test.go +++ b/internal/control/controller_test.go @@ -11,6 +11,7 @@ import ( "os" "path/filepath" "reflect" + "slices" "strings" "sync" "sync/atomic" @@ -273,9 +274,9 @@ func requestMessagesText(messages []provider.Message) string { } func lastUserMessage(messages []provider.Message) string { - for i := len(messages) - 1; i >= 0; i-- { - if messages[i].Role == provider.RoleUser { - return messages[i].Content + for _, v := range slices.Backward(messages) { + if v.Role == provider.RoleUser { + return v.Content } } return "" @@ -1022,7 +1023,7 @@ func TestRecoverInterruptedTurnAfterCompactionRelocatesVisibleTurn(t *testing.T) path := filepath.Join(dir, "compacted-crash.jsonl") orig := agent.NewSession("sys") - for i := 0; i < 3; i++ { + for range 3 { orig.Add(provider.Message{Role: provider.RoleUser, Content: "old task"}) orig.Add(provider.Message{Role: provider.RoleAssistant, Content: "old answer"}) } @@ -1347,12 +1348,10 @@ func TestConcurrentSnapshotsShareSingleRecoveryHandoff(t *testing.T) { const racingSnapshots = 8 var wg sync.WaitGroup errs := make(chan error, racingSnapshots) - for i := 0; i < racingSnapshots; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range racingSnapshots { + wg.Go(func() { errs <- c.Snapshot() - }() + }) } select { @@ -1664,7 +1663,7 @@ func TestSnapshotConflictAdoptionResetsRewriteBaseline(t *testing.T) { c := New(Options{Executor: exec, SessionDir: dir, SessionPath: path, Label: "test"}) // Rewrites the stale controller never persisted before it noticed the // newer transcript; adoption must discard this counter with the session. - for i := 0; i < 3; i++ { + for range 3 { stale.IncrementRewrite() } @@ -1723,9 +1722,7 @@ func TestConcurrentCompactionAndAutosaveNeverBranch(t *testing.T) { stop := make(chan struct{}) var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { for { select { case <-stop: @@ -1735,7 +1732,7 @@ func TestConcurrentCompactionAndAutosaveNeverBranch(t *testing.T) { _ = c.SnapshotActivity() } } - }() + }) for i := 1; i <= 40; i++ { sess.Add(provider.Message{Role: provider.RoleUser, Content: fmt.Sprintf("turn-%d", i)}) if i%4 == 0 { @@ -2077,9 +2074,9 @@ func (s *noticeSink) notices() []string { func (s *noticeSink) lastNotice() (event.Event, bool) { s.mu.Lock() defer s.mu.Unlock() - for i := len(s.events) - 1; i >= 0; i-- { - if s.events[i].Kind == event.Notice { - return s.events[i], true + for _, v := range slices.Backward(s.events) { + if v.Kind == event.Notice { + return v, true } } return event.Event{}, false @@ -2981,12 +2978,12 @@ func TestConnectConfiguredProjectMCPIsTrustedByDefault(t *testing.T) { }) })) defer server.Close() - if err := os.WriteFile(filepath.Join(workspace, "reasonix.toml"), []byte(fmt.Sprintf(` + if err := os.WriteFile(filepath.Join(workspace, "reasonix.toml"), fmt.Appendf(nil, ` [[plugins]] name = "project-docs" type = "http" url = %q -`, server.URL)), 0o644); err != nil { +`, server.URL), 0o644); err != nil { t.Fatal(err) } @@ -4096,7 +4093,7 @@ func TestApprovalPersistenceFailureKeepsSessionGrant(t *testing.T) { c.Approve(<-ids, true, true, true) }() - for i := 0; i < 2; i++ { + for i := range 2 { allow, remember, err := gateApprover{c}.Approve(context.Background(), "bash", "go test ./...", nil) if err != nil || !allow || remember { t.Fatalf("Approve call %d = (%v,%v,%v), want session-allowed despite persistence failure", i, allow, remember, err) @@ -4446,10 +4443,10 @@ func TestRunGuardedParksReplacementUntilTurnDoneReturns(t *testing.T) { releaseTurnDone := make(chan struct{}) firstBodyDone := make(chan struct{}) secondBodyRan := make(chan struct{}, 2) - var turnDones int32 + var turnDones atomic.Int32 c := New(Options{Sink: event.FuncSink(func(e event.Event) { if e.Kind == event.TurnDone { - if atomic.AddInt32(&turnDones, 1) == 1 { + if turnDones.Add(1) == 1 { close(firstTurnDone) <-releaseTurnDone } @@ -4496,7 +4493,7 @@ func TestRunGuardedParksReplacementUntilTurnDoneReturns(t *testing.T) { if c.Running() { t.Fatal("controller remained busy after the parked turn completed") } - if got := atomic.LoadInt32(&turnDones); got != 2 { + if got := turnDones.Load(); got != 2 { t.Fatalf("TurnDone emitted %d times, want 2 (one per turn)", got) } select { @@ -4508,13 +4505,13 @@ func TestRunGuardedParksReplacementUntilTurnDoneReturns(t *testing.T) { func TestRunGuardedPanicDoesNotDoubleEmitTurnDone(t *testing.T) { sess := agent.NewSession("sys") - var count int32 + var count atomic.Int32 events := make(chan event.Event, 8) c := New(Options{ Runner: appendingRunner{session: sess}, Sink: event.FuncSink(func(e event.Event) { if e.Kind == event.TurnDone { - atomic.AddInt32(&count, 1) + count.Add(1) } events <- e }), @@ -4530,10 +4527,10 @@ func TestRunGuardedPanicDoesNotDoubleEmitTurnDone(t *testing.T) { for { select { case <-events: - n := atomic.LoadInt32(&count) + n := count.Load() if n >= 1 { time.Sleep(50 * time.Millisecond) - n2 := atomic.LoadInt32(&count) + n2 := count.Load() if n2 > 1 { t.Fatalf("TurnDone emitted %d times, expected 1", n2) } diff --git a/internal/control/goal_concurrency_test.go b/internal/control/goal_concurrency_test.go index f867b862c6..2cbf96ea0b 100644 --- a/internal/control/goal_concurrency_test.go +++ b/internal/control/goal_concurrency_test.go @@ -26,9 +26,7 @@ func TestGoalStateWritesAreConcurrencySafe(t *testing.T) { stop := make(chan struct{}) var readers sync.WaitGroup - readers.Add(1) - go func() { - defer readers.Done() + readers.Go(func() { for { select { case <-stop: @@ -40,7 +38,7 @@ func TestGoalStateWritesAreConcurrencySafe(t *testing.T) { _ = c.GoalStatus() // takes c.mu } } - }() + }) var writers sync.WaitGroup for w := range 8 { diff --git a/internal/control/goal_runtime_test.go b/internal/control/goal_runtime_test.go index e595530ef6..299e3d9ef8 100644 --- a/internal/control/goal_runtime_test.go +++ b/internal/control/goal_runtime_test.go @@ -211,7 +211,7 @@ func TestTurnTokenNoProgressPausesAndResumeExtendsBudget(t *testing.T) { t.Run("turn budget pauses", func(t *testing.T) { g := newMachine() - for i := 0; i < g.turnsLimit; i++ { + for i := range g.turnsLimit { // Progress changes every turn so only the turn budget can fire. res := g.advance(in(&goalTurnReport{status: GoalStatusRunning, reason: "keep going"}, "s", "s"+fmt.Sprint(i))) if res.cont && i == g.turnsLimit-1 { @@ -238,7 +238,7 @@ func TestTurnTokenNoProgressPausesAndResumeExtendsBudget(t *testing.T) { t.Run("no-progress pauses", func(t *testing.T) { g := newMachine() - for i := 0; i < g.noProgressLimit; i++ { + for range g.noProgressLimit { g.advance(in(&goalTurnReport{status: GoalStatusRunning, reason: "still working"}, "sig", "sig")) } if g.status != GoalStatusBlocked || g.stopCause != stopCauseNoProgress { @@ -248,7 +248,7 @@ func TestTurnTokenNoProgressPausesAndResumeExtendsBudget(t *testing.T) { t.Run("host-verifiable progress resets no-progress", func(t *testing.T) { g := newMachine() - for i := 0; i < g.noProgressLimit-1; i++ { + for range g.noProgressLimit - 1 { g.advance(in(&goalTurnReport{status: GoalStatusRunning, reason: "still working"}, "sig", "sig")) } res := g.advance(in(&goalTurnReport{status: GoalStatusRunning, reason: "progress!"}, "sig", "sig2")) @@ -262,7 +262,7 @@ func TestTurnTokenNoProgressPausesAndResumeExtendsBudget(t *testing.T) { t.Run("resume extends turn budget once", func(t *testing.T) { g := newMachine() - for i := 0; i < g.turnsLimit; i++ { + for range g.turnsLimit { g.advance(in(&goalTurnReport{status: GoalStatusRunning, reason: "keep going"}, "s", "s")) } beforeLimit := g.turnsLimit diff --git a/internal/control/goal_test.go b/internal/control/goal_test.go index 5fc3627b93..db7f38e98b 100644 --- a/internal/control/goal_test.go +++ b/internal/control/goal_test.go @@ -1213,7 +1213,7 @@ func TestRepeatedCompleteWithIncompleteTodosPausesOnBudget(t *testing.T) { // The write-class budget is 20 turns; give the scripted provider a full // pair per turn so the model keeps reporting complete each time. turns := flattenTurns() - for i := 0; i < 21; i++ { + for range 21 { turns = append(turns, goalToolTurn(GoalStatusComplete, "", "")...) } prov := &scriptedTurns{turns: turns} diff --git a/internal/control/imagecompress.go b/internal/control/imagecompress.go index f7082db520..6e69e30bb7 100644 --- a/internal/control/imagecompress.go +++ b/internal/control/imagecompress.go @@ -63,15 +63,9 @@ func compressForVision(raw []byte, mime string) ([]byte, string) { // aspect ratio (each side at least 1px). func scaledDims(w, h, m int) (int, int) { if w >= h { - nh := h * m / w - if nh < 1 { - nh = 1 - } + nh := max(h*m/w, 1) return m, nh } - nw := w * m / h - if nw < 1 { - nw = 1 - } + nw := max(w*m/h, 1) return nw, m } diff --git a/internal/control/imagecompress_test.go b/internal/control/imagecompress_test.go index 46cddfe80c..b219f0042b 100644 --- a/internal/control/imagecompress_test.go +++ b/internal/control/imagecompress_test.go @@ -12,8 +12,8 @@ import ( func makeTestPNG(t *testing.T, w, h int) []byte { t.Helper() img := image.NewRGBA(image.Rect(0, 0, w, h)) - for y := 0; y < h; y++ { - for x := 0; x < w; x++ { + for y := range h { + for x := range w { img.Set(x, y, color.RGBA{R: uint8(x), G: uint8(y), B: uint8(x ^ y), A: 255}) } } diff --git a/internal/control/mcp.go b/internal/control/mcp.go index aa2159144b..a26832cc04 100644 --- a/internal/control/mcp.go +++ b/internal/control/mcp.go @@ -3,6 +3,7 @@ package control import ( "context" "fmt" + "slices" "sync" "time" @@ -173,12 +174,7 @@ func (m *mcpManager) serverNames() []string { // hasServer reports whether a server is live. func (m *mcpManager) hasServer(name string) bool { - for _, n := range m.serverNames() { - if n == name { - return true - } - } - return false + return slices.Contains(m.serverNames(), name) } // prompts lists the live MCP prompts (nil when no host is connected). diff --git a/internal/control/memory_command.go b/internal/control/memory_command.go index 978bd8417b..53b74940d5 100644 --- a/internal/control/memory_command.go +++ b/internal/control/memory_command.go @@ -106,8 +106,8 @@ func MemoryCommandText(api MemoryControl, input string) string { func parseMemoryCommand(input string) (subcommand, rest string) { input = strings.TrimSpace(input) - if strings.HasPrefix(input, "/memory") { - input = strings.TrimSpace(strings.TrimPrefix(input, "/memory")) + if after, ok := strings.CutPrefix(input, "/memory"); ok { + input = strings.TrimSpace(after) } if input == "" { return "", "" diff --git a/internal/control/memory_test.go b/internal/control/memory_test.go index 0f377381a4..0f8ac7e4e8 100644 --- a/internal/control/memory_test.go +++ b/internal/control/memory_test.go @@ -160,9 +160,7 @@ func TestMemoryWritesConcurrencySafe(t *testing.T) { stop := make(chan struct{}) var readers sync.WaitGroup - readers.Add(1) - go func() { - defer readers.Done() + readers.Go(func() { for { select { case <-stop: @@ -173,7 +171,7 @@ func TestMemoryWritesConcurrencySafe(t *testing.T) { _ = c.Memory() // takes c.mu, returns the snapshot pointer } } - }() + }) var writersWG sync.WaitGroup for w := range writers { diff --git a/internal/control/plan_seed_test.go b/internal/control/plan_seed_test.go index cedd75751f..5ec87536dc 100644 --- a/internal/control/plan_seed_test.go +++ b/internal/control/plan_seed_test.go @@ -2,6 +2,7 @@ package control import ( "encoding/json" + "strings" "testing" "reasonix/internal/agent" @@ -140,11 +141,11 @@ func TestPlanTodosJSONAlwaysSatisfiesSerialContract(t *testing.T) { } func TestParsePlanTodosCapsAtTwenty(t *testing.T) { - plan := "" - for i := 0; i < 30; i++ { - plan += "- item\n" + var plan strings.Builder + for range 30 { + plan.WriteString("- item\n") } - if got := parsePlanTodos(plan); len(got) != 20 { + if got := parsePlanTodos(plan.String()); len(got) != 20 { t.Fatalf("got %d todos, want cap of 20", len(got)) } } diff --git a/internal/control/plan_todos.go b/internal/control/plan_todos.go index 0f9ed8d69c..2ed77d988e 100644 --- a/internal/control/plan_todos.go +++ b/internal/control/plan_todos.go @@ -5,6 +5,7 @@ package control import ( "encoding/json" + "slices" "strings" "reasonix/internal/event" @@ -134,7 +135,7 @@ func completedPlanTodosJSON(args string) string { // todo_write calls as it executes. func parsePlanTodos(plan string) []seedTodo { var todos []seedTodo - for _, raw := range strings.Split(plan, "\n") { + for raw := range strings.SplitSeq(plan, "\n") { item, level, ok := listItem(raw) if !ok { continue @@ -189,8 +190,8 @@ func (c *Controller) hasTodoUpdateSince(start int) bool { func latestTodoArgsSince(msgs []provider.Message, start int) (string, bool) { for i := len(msgs) - 1; i >= start; i-- { - for j := len(msgs[i].ToolCalls) - 1; j >= 0; j-- { - tc := msgs[i].ToolCalls[j] + for _, v := range slices.Backward(msgs[i].ToolCalls) { + tc := v if tc.Name == "todo_write" { return tc.Arguments, true } diff --git a/internal/control/planner_gate.go b/internal/control/planner_gate.go index be07f1faf0..e791045f51 100644 --- a/internal/control/planner_gate.go +++ b/internal/control/planner_gate.go @@ -3,6 +3,7 @@ package control import ( "context" "regexp" + "slices" "strings" "unicode" "unicode/utf8" @@ -247,8 +248,8 @@ func normalizePlannerText(text string) string { func hasLeadingDirective(lower string, directives []string) bool { lower = strings.TrimSpace(lower) for _, polite := range []string{"please ", "please, ", "请", "请先", "麻烦", "麻烦先"} { - if strings.HasPrefix(lower, polite) { - lower = strings.TrimSpace(strings.TrimPrefix(lower, polite)) + if after, ok := strings.CutPrefix(lower, polite); ok { + lower = strings.TrimSpace(after) break } } @@ -582,14 +583,9 @@ func containsLexicalTerm(s, term string) bool { if containsNonASCII(term) || strings.ContainsAny(term, " -_/") { return strings.Contains(s, term) } - for _, token := range strings.FieldsFunc(s, func(r rune) bool { + return slices.Contains(strings.FieldsFunc(s, func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '_' - }) { - if token == term { - return true - } - } - return false + }), term) } func containsNonASCII(s string) bool { diff --git a/internal/control/recovery.go b/internal/control/recovery.go index ade821a589..9d72c1699d 100644 --- a/internal/control/recovery.go +++ b/internal/control/recovery.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log/slog" + "slices" "strings" "unicode/utf8" @@ -117,9 +118,9 @@ func (c *Controller) initRecoveryGate(reviewer recovery.Reviewer, headless bool) return "" } msgs := c.executor.Session().Snapshot() - for i := len(msgs) - 1; i >= 0; i-- { - if string(msgs[i].Role) == "user" && strings.TrimSpace(msgs[i].Content) != "" { - text := agent.UserMessageText(msgs[i]) + for _, v := range slices.Backward(msgs) { + if string(v.Role) == "user" && strings.TrimSpace(v.Content) != "" { + text := agent.UserMessageText(v) if len(text) > 800 { return text[:800] + "…" } diff --git a/internal/control/refs.go b/internal/control/refs.go index 2cff158a5f..8e07d9b7c9 100644 --- a/internal/control/refs.go +++ b/internal/control/refs.go @@ -137,7 +137,7 @@ func EscapeRefPath(path string) string { } var b strings.Builder b.Grow(len(path) + 8) - for i := 0; i < len(path); i++ { + for i := range len(path) { if path[i] == ' ' || path[i] == '\t' { b.WriteByte('\\') } @@ -154,7 +154,7 @@ func UnescapeRefPath(path string) string { } var b strings.Builder b.Grow(len(path)) - for i := 0; i < len(path); i++ { + for i := range len(path) { if path[i] == '\\' && i+1 < len(path) && (path[i+1] == ' ' || path[i+1] == '\t') { continue } diff --git a/internal/control/turn_orchestrator_test.go b/internal/control/turn_orchestrator_test.go index 98ebcc523c..520050e933 100644 --- a/internal/control/turn_orchestrator_test.go +++ b/internal/control/turn_orchestrator_test.go @@ -921,7 +921,7 @@ func TestTurnOrchestratorInterruptedAfterCompactionRelocatesVisibleTurn(t *testi } { t.Run(tc.name, func(t *testing.T) { sess := agent.NewSession("system") - for i := 0; i < 3; i++ { + for range 3 { sess.Add(provider.Message{Role: provider.RoleUser, Content: "old task"}) sess.Add(provider.Message{Role: provider.RoleAssistant, Content: "old answer"}) } diff --git a/internal/crashreport/cli.go b/internal/crashreport/cli.go index 8a36cb0635..aeaee17d6b 100644 --- a/internal/crashreport/cli.go +++ b/internal/crashreport/cli.go @@ -321,7 +321,7 @@ func sanitizeStack(stack string) string { func topFrame(stack string) string { fallback := "" functionName := "" - for _, line := range strings.Split(stack, "\n") { + for line := range strings.SplitSeq(stack, "\n") { line = strings.TrimSpace(line) if strings.HasPrefix(line, "/") && strings.Contains(line, ".go:") { frame := line @@ -337,8 +337,8 @@ func topFrame(stack string) string { functionName = "" continue } - if strings.HasSuffix(line, "(...)") { - functionName = strings.TrimSpace(strings.TrimSuffix(line, "(...)")) + if before, ok := strings.CutSuffix(line, "(...)"); ok { + functionName = strings.TrimSpace(before) } } return clip(fallback, 300) diff --git a/internal/crashreport/cli_test.go b/internal/crashreport/cli_test.go index 1d6a54b6f5..bca4ded010 100644 --- a/internal/crashreport/cli_test.go +++ b/internal/crashreport/cli_test.go @@ -74,7 +74,7 @@ func TestCapturePanicWritesBoundedSanitizedReport(t *testing.T) { t.Fatalf("report mode=%v", info.Mode().Perm()) } - for i := 0; i < maxReports+5; i++ { + for i := range maxReports + 5 { if err := CapturePanic(home, "v1.20.0", i, []byte(stack)); err != nil { t.Fatal(err) } @@ -139,7 +139,7 @@ func TestConcurrentCaptureKeepsQueueBounded(t *testing.T) { const writers = 32 var wg sync.WaitGroup start := make(chan struct{}) - for i := 0; i < writers; i++ { + for i := range writers { wg.Add(1) go func(value int) { defer wg.Done() @@ -169,7 +169,7 @@ func TestCapturePanicPrunesOnlyCurrentReportFormat(t *testing.T) { t.Fatal(err) } - for i := 0; i < maxReports+1; i++ { + for i := range maxReports + 1 { if err := CapturePanic(home, "v1.20.0", i, []byte("reasonix.run()\n\t/home/alice/main.go:12")); err != nil { t.Fatal(err) } diff --git a/internal/desktoplauncher/launcher.go b/internal/desktoplauncher/launcher.go index 660ff097d1..48d4905799 100644 --- a/internal/desktoplauncher/launcher.go +++ b/internal/desktoplauncher/launcher.go @@ -152,7 +152,7 @@ func siblingDesktop(installRoot string) string { func StripLegacyLaunchArgs(args []string) []string { out := make([]string, 0, len(args)) skipNext := false - for i := 0; i < len(args); i++ { + for i := range args { if skipNext { skipNext = false continue diff --git a/internal/diff/diff_extra_test.go b/internal/diff/diff_extra_test.go index a5e3be09b4..7053ac6fb1 100644 --- a/internal/diff/diff_extra_test.go +++ b/internal/diff/diff_extra_test.go @@ -134,7 +134,7 @@ func TestBuildWhitespaceOnly(t *testing.T) { func TestBuildLargeFile(t *testing.T) { // Build a large file with one changed line in the middle. var oldB, newB strings.Builder - for i := 0; i < 1000; i++ { + for i := range 1000 { oldB.WriteString("line\n") if i == 500 { newB.WriteString("CHANGED\n") diff --git a/internal/diff/largediff_test.go b/internal/diff/largediff_test.go index c94f734dd2..601d923996 100644 --- a/internal/diff/largediff_test.go +++ b/internal/diff/largediff_test.go @@ -15,7 +15,7 @@ import ( func TestLargeRewriteBoundedCost(t *testing.T) { var oldB, newB strings.Builder const n = 6000 - for i := 0; i < n; i++ { + for i := range n { fmt.Fprintf(&oldB, "old line %d\n", i) fmt.Fprintf(&newB, "totally different new line %d\n", i) } @@ -49,7 +49,7 @@ func TestLargeRewriteBoundedCost(t *testing.T) { func TestSmallEditOnLargeFileStillDiffs(t *testing.T) { var oldB strings.Builder const n = 8000 - for i := 0; i < n; i++ { + for i := range n { fmt.Fprintf(&oldB, "line %d\n", i) } old := oldB.String() diff --git a/internal/doctor/session_redact.go b/internal/doctor/session_redact.go index ed32a46d02..54fbabb1ea 100644 --- a/internal/doctor/session_redact.go +++ b/internal/doctor/session_redact.go @@ -157,8 +157,8 @@ func sessionRedactionLeaseHeld(sessionPath string) bool { if agent.SessionLeaseHeld(sessionPath) { return true } - if strings.HasSuffix(sessionPath, ".guardian.jsonl") { - parent := strings.TrimSuffix(sessionPath, ".guardian.jsonl") + ".jsonl" + if before, ok := strings.CutSuffix(sessionPath, ".guardian.jsonl"); ok { + parent := before + ".jsonl" return agent.SessionLeaseHeld(parent) } return false diff --git a/internal/doctor/skill_health.go b/internal/doctor/skill_health.go index 82ffa97f0f..cb2a40dd8c 100644 --- a/internal/doctor/skill_health.go +++ b/internal/doctor/skill_health.go @@ -58,8 +58,8 @@ func CollectSkillHealthWarnings(opts SkillHealthOptions) []string { if dep == "" { continue } - if strings.HasPrefix(dep, "mcp-server:") { - srv := strings.TrimPrefix(dep, "mcp-server:") + if after, ok := strings.CutPrefix(dep, "mcp-server:"); ok { + srv := after if !pluginNames[srv] { out = append(out, fmt.Sprintf("skill %q requires %s but that MCP server is not configured", name, dep)) } else if reason, ok := opts.FailedServers[srv]; ok && reason != "" { diff --git a/internal/environment/probe.go b/internal/environment/probe.go index 011e1d8a7c..6186c97e2e 100644 --- a/internal/environment/probe.go +++ b/internal/environment/probe.go @@ -292,8 +292,8 @@ func sortResults(results []ProbeResult) { func firstLine(s string) string { s = strings.TrimSpace(s) - if i := strings.IndexByte(s, '\n'); i >= 0 { - return strings.TrimRight(s[:i], "\r") + if before, _, ok := strings.Cut(s, "\n"); ok { + return strings.TrimRight(before, "\r") } return s } diff --git a/internal/environment/probe_test.go b/internal/environment/probe_test.go index efc4ca5e62..c04054fc8f 100644 --- a/internal/environment/probe_test.go +++ b/internal/environment/probe_test.go @@ -251,7 +251,7 @@ func TestRunProbesCacheSeparatesOverrides(t *testing.T) { func TestFormatSectionLimitsToolOutput(t *testing.T) { overrides := map[string]string{} var results []ProbeResult - for i := 0; i < maxRenderedTools+2; i++ { + for i := range maxRenderedTools + 2 { name := fmt.Sprintf("tool%02d", i) overrides[name] = "/bin/" + name results = append(results, ProbeResult{Binary: name, Found: true, Output: "ok"}) diff --git a/internal/event/event_test.go b/internal/event/event_test.go index cecaa3a8d2..70e7534453 100644 --- a/internal/event/event_test.go +++ b/internal/event/event_test.go @@ -280,12 +280,10 @@ func TestFuncSinkForwardsEachConcurrentEmit(t *testing.T) { mu.Unlock() }) var wg sync.WaitGroup - for i := 0; i < 100; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range 100 { + wg.Go(func() { sink.Emit(Event{Kind: Text}) - }() + }) } wg.Wait() mu.Lock() diff --git a/internal/eventwire/wire.go b/internal/eventwire/wire.go index 1be6795a04..b039405b54 100644 --- a/internal/eventwire/wire.go +++ b/internal/eventwire/wire.go @@ -451,7 +451,7 @@ func ToWireCacheDiagnostics(d *event.CacheDiagnostics) *CacheDiagnostics { // generator; callers receive a copy and may sort it without mutating eventwire. func KindNames() []string { names := make([]string, 0, int(event.KindCount)) - for kind := event.Kind(0); kind < event.KindCount; kind++ { + for kind := range event.KindCount { if name, ok := kindNames[kind]; ok { names = append(names, name) } diff --git a/internal/eventwire/wire_test.go b/internal/eventwire/wire_test.go index 7f414f3885..b5ebd3a9fe 100644 --- a/internal/eventwire/wire_test.go +++ b/internal/eventwire/wire_test.go @@ -96,7 +96,7 @@ func TestToWireNoticeCarriesDecisionReceipt(t *testing.T) { } func TestKindNamesComplete(t *testing.T) { - for k := event.Kind(0); k < event.KindCount; k++ { + for k := range event.KindCount { if ToWire(event.Event{Kind: k}).Kind == "" { t.Fatalf("kind %d has no wire name", k) } @@ -105,7 +105,7 @@ func TestKindNamesComplete(t *testing.T) { func TestDesktopWireEventKindTypeCoversSharedKinds(t *testing.T) { ts := readDesktopTypes(t) - for k := event.Kind(0); k < event.KindCount; k++ { + for k := range event.KindCount { kind := ToWire(event.Event{Kind: k}).Kind if !strings.Contains(ts, `"`+kind+`"`) { t.Fatalf("desktop WireEvent EventKind is missing %q", kind) diff --git a/internal/evidence/evidence.go b/internal/evidence/evidence.go index fd38d40555..7767ec50c4 100644 --- a/internal/evidence/evidence.go +++ b/internal/evidence/evidence.go @@ -7,6 +7,7 @@ import ( "fmt" "path/filepath" "runtime" + "slices" "strconv" "strings" "sync" @@ -765,10 +766,7 @@ func (l *Ledger) HasSuccessfulCommandAfter(command string, after int) bool { if l == nil || command == "" { return false } - start := after + 1 - if start < 0 { - start = 0 - } + start := max(after+1, 0) l.mu.Lock() defer l.mu.Unlock() @@ -785,10 +783,7 @@ func (l *Ledger) HasSuccessfulCompleteStepAfter(after int) bool { if l == nil { return false } - start := after + 1 - if start < 0 { - start = 0 - } + start := max(after+1, 0) l.mu.Lock() defer l.mu.Unlock() @@ -810,10 +805,7 @@ func (l *Ledger) HasSuccessfulDeliverySignoffAfter(after int) bool { if l == nil { return false } - start := after + 1 - if start < 0 { - start = 0 - } + start := max(after+1, 0) l.mu.Lock() receipts := append([]Receipt(nil), l.receipts...) @@ -851,10 +843,7 @@ func (l *Ledger) HasSuccessfulReviewAfter(after int) bool { if l == nil { return false } - start := after + 1 - if start < 0 { - start = 0 - } + start := max(after+1, 0) l.mu.Lock() receipts := append([]Receipt(nil), l.receipts...) @@ -875,10 +864,7 @@ func (l *Ledger) HasHostReviewCoverageAfter(after int, requiredPaths []string) b if l == nil { return false } - start := after + 1 - if start < 0 { - start = 0 - } + start := max(after+1, 0) l.mu.Lock() receipts := append([]Receipt(nil), l.receipts...) l.mu.Unlock() @@ -1036,8 +1022,8 @@ func (l *Ledger) IncompleteLatestTodos() ([]TodoStepMatch, bool) { } l.mu.Lock() defer l.mu.Unlock() - for i := len(l.receipts) - 1; i >= 0; i-- { - r := l.receipts[i] + for _, v := range slices.Backward(l.receipts) { + r := v if !r.Success || r.ToolName != "todo_write" { continue } @@ -1251,10 +1237,7 @@ func (l *Ledger) HasSuccessfulAnchorRefreshReadAfter(paths []string, after int) if l == nil || len(wanted) == 0 { return false } - start := after + 1 - if start < 0 { - start = 0 - } + start := max(after+1, 0) l.mu.Lock() defer l.mu.Unlock() @@ -1331,8 +1314,8 @@ func (l *Ledger) MatchLatestTodoStep(step string) (TodoStepMatch, bool) { } l.mu.Lock() defer l.mu.Unlock() - for i := len(l.receipts) - 1; i >= 0; i-- { - r := l.receipts[i] + for _, v := range slices.Backward(l.receipts) { + r := v if !r.Success || r.ToolName != "todo_write" { continue } @@ -1348,8 +1331,8 @@ func (l *Ledger) LatestTodos() ([]TodoItem, bool) { } l.mu.Lock() defer l.mu.Unlock() - for i := len(l.receipts) - 1; i >= 0; i-- { - r := l.receipts[i] + for _, v := range slices.Backward(l.receipts) { + r := v if r.Success && r.ToolName == "todo_write" { return append([]TodoItem(nil), r.Todos...), true } @@ -1374,8 +1357,8 @@ func (l *Ledger) UnverifiedCompletedTodos(current []TodoItem) (missing []TodoSte var previous []TodoItem baseline := -1 - for i := len(receipts) - 1; i >= 0; i-- { - r := receipts[i] + for i, v := range slices.Backward(receipts) { + r := v if !r.Success || r.ToolName != "todo_write" { continue } @@ -1444,10 +1427,7 @@ func hasFailedCompleteStepRecoveryForTodo(receipts []Receipt, baseline int, inde // Recovery only trusts progress that happened before the failed sign-off. // Later unrelated work must not retroactively authorize an earlier completion. func hasSuccessfulProgressBeforeReceipt(receipts []Receipt, baseline int, before int) bool { - start := baseline + 1 - if start < 0 { - start = 0 - } + start := max(baseline+1, 0) for i := start; i < before && i < len(receipts); i++ { r := receipts[i] if !r.Success || r.ToolName == "todo_write" || r.ToolName == "complete_step" || r.Read { @@ -1811,12 +1791,7 @@ func bashCommandUsesOpaqueInlineInterpreter(command string) bool { if !ok { return false } - for _, segment := range segments { - if bashSegmentUsesOpaqueInlineInterpreter(segment) { - return true - } - } - return false + return slices.ContainsFunc(segments, bashSegmentUsesOpaqueInlineInterpreter) } func bashSegmentUsesOpaqueInlineInterpreter(segment string) bool { @@ -2018,12 +1993,7 @@ func bashSegmentIsVerification(fields []string) bool { return true } if args[0] == "test" { - for _, arg := range args[1:] { - if goTestFlagWritesFile(arg) { - return false - } - } - return true + return !slices.ContainsFunc(args[1:], goTestFlagWritesFile) } // A package pattern can expand to one main package, so even `go build // ./...` may write a workspace binary. Package expansion and inherited @@ -2037,12 +2007,7 @@ func bashSegmentIsVerification(fields []string) bool { case "tsc": return tscSegmentIsVerification(args) case "mypy": - for _, arg := range args { - if mypyFlagWritesReport(arg) { - return false - } - } - return true + return !slices.ContainsFunc(args, mypyFlagWritesReport) case "npm", "pnpm", "yarn", "bun", "cargo": if len(args) > 0 && hasCommandArg(args[:1], "test", "check", "lint", "clippy") { return true @@ -2241,12 +2206,7 @@ func nodeSegmentIsVerification(args []string) bool { case "--test": // Match the repository's treatment of other conventional test runners, // but fail closed on test-runner and Node runtime flags that write files. - for _, arg := range args[1:] { - if nodeTestFlagWritesFile(arg) { - return false - } - } - return true + return !slices.ContainsFunc(args[1:], nodeTestFlagWritesFile) default: return false } @@ -2541,8 +2501,8 @@ func argNamesPath(arg, needle string) bool { if tok == needle || strings.HasSuffix(tok, "/"+needle) { return true } - if i := strings.Index(tok, ":"); i >= 0 { - rest := tok[i+1:] + if _, after, ok := strings.Cut(tok, ":"); ok { + rest := after if rest == needle || strings.HasSuffix(rest, "/"+needle) { return true } @@ -2788,8 +2748,8 @@ func hasSuccessfulCompleteStepForTodo(receipts []Receipt, index int, current []T } func latestTodoStep(step string, receipts []Receipt) TodoStepMatch { - for i := len(receipts) - 1; i >= 0; i-- { - r := receipts[i] + for _, v := range slices.Backward(receipts) { + r := v if !r.Success || r.ToolName != "todo_write" { continue } diff --git a/internal/evidence/review_report.go b/internal/evidence/review_report.go index 2e2276406f..df0638e831 100644 --- a/internal/evidence/review_report.go +++ b/internal/evidence/review_report.go @@ -161,10 +161,7 @@ func (l *Ledger) HasStructuredReviewAfter(kind ReviewKind, after int, requiredPa if l == nil { return false, false, nil } - start := after + 1 - if start < 0 { - start = 0 - } + start := max(after+1, 0) l.mu.Lock() defer l.mu.Unlock() for i := start; i < len(l.receipts); i++ { diff --git a/internal/evidence/risk.go b/internal/evidence/risk.go index 2118695f53..7fa72a5cc7 100644 --- a/internal/evidence/risk.go +++ b/internal/evidence/risk.go @@ -36,10 +36,7 @@ var highRiskToolHints = []string{ // Medium: ordinary production code or limited multi-file edits. // High: security-sensitive surfaces, opaque mutations, or 10+ paths. func ClassifyMutationRisk(receipts []Receipt, after int) RiskLevel { - start := after + 1 - if start < 0 { - start = 0 - } + start := max(after+1, 0) var paths []string seen := map[string]bool{} opaque := false @@ -123,10 +120,7 @@ func (l *Ledger) PathsSince(after int) []string { if l == nil { return nil } - start := after - if start < 0 { - start = 0 - } + start := max(after, 0) l.mu.Lock() defer l.mu.Unlock() seen := map[string]bool{} diff --git a/internal/extension/benchmark_test.go b/internal/extension/benchmark_test.go index cba4e5d39e..48996fdb30 100644 --- a/internal/extension/benchmark_test.go +++ b/internal/extension/benchmark_test.go @@ -2,7 +2,7 @@ package extension import ( "context" - "sort" + "slices" "testing" "time" ) @@ -17,7 +17,7 @@ func BenchmarkExtensionKernelStartup(b *testing.B) { }) contributions := make([]Contribution, 0, 64) - for i := 0; i < 64; i++ { + for i := range 64 { contributions = append(contributions, Contribution{ Kind: KindInterceptor, ID: string(PointToolBefore), @@ -61,7 +61,7 @@ func benchmarkBuildLatency(b *testing.B, builder *Builder) { } } b.StopTimer() - sort.Slice(samples, func(i, j int) bool { return samples[i] < samples[j] }) + slices.Sort(samples) if len(samples) == 0 { return } diff --git a/internal/extension/builder.go b/internal/extension/builder.go index 4a6674ccae..6e163675a3 100644 --- a/internal/extension/builder.go +++ b/internal/extension/builder.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "maps" "strings" "reasonix/internal/provider" @@ -408,9 +409,7 @@ func (b *Builder) assemble(resolved []Contribution, replacements map[Slot]Contri } repl := make(map[Slot]ContributionSource, len(replacements)) - for slot, owner := range replacements { - repl[slot] = owner - } + maps.Copy(repl, replacements) diagnostics := make([]string, 0, len(conflicts)) for i := range conflicts { diff --git a/internal/extension/builder_test.go b/internal/extension/builder_test.go index 27be3bcc9b..38f9ac7921 100644 --- a/internal/extension/builder_test.go +++ b/internal/extension/builder_test.go @@ -71,7 +71,7 @@ func TestBuildDeterminism(t *testing.T) { hash string } var reference *fingerprint - for seed := int64(0); seed < 100; seed++ { + for seed := range int64(100) { r := rand.New(rand.NewSource(seed)) perm := r.Perm(len(contributors)) b := NewBuilder().WithSystemPrompt("system prompt v1").WithGeneration(7) diff --git a/internal/extension/dispatch/benchmark_test.go b/internal/extension/dispatch/benchmark_test.go index 1c17045629..7901d4e4c5 100644 --- a/internal/extension/dispatch/benchmark_test.go +++ b/internal/extension/dispatch/benchmark_test.go @@ -3,7 +3,7 @@ package dispatch import ( "context" "encoding/json" - "sort" + "slices" "strconv" "testing" "time" @@ -37,7 +37,6 @@ func BenchmarkDispatchLatency(b *testing.B) { }) }) for _, count := range []int{1, 4} { - count := count b.Run("Turn/NoopInterceptors"+strconv.Itoa(count), func(b *testing.B) { d := benchmarkDispatcher(extension.PointInputReceive, count) payload := InputPayload{Text: "hello"} @@ -60,7 +59,7 @@ func BenchmarkDispatchLatency(b *testing.B) { func benchmarkDispatcher(point extension.InterceptorPoint, count int) *Dispatcher { chain := make([]extension.Contribution, 0, count) clients := make(map[string]Client, count) - for i := 0; i < count; i++ { + for i := range count { pluginID := string(rune('a' + i)) chain = append(chain, extension.Contribution{ Kind: extension.KindInterceptor, ID: string(point), @@ -87,7 +86,7 @@ func benchmarkLatency(b *testing.B, fn func() error) { } } b.StopTimer() - sort.Slice(samples, func(i, j int) bool { return samples[i] < samples[j] }) + slices.Sort(samples) if len(samples) == 0 { return } diff --git a/internal/extension/dispatch/dispatch_test.go b/internal/extension/dispatch/dispatch_test.go index 622d8185c6..e3afc7b743 100644 --- a/internal/extension/dispatch/dispatch_test.go +++ b/internal/extension/dispatch/dispatch_test.go @@ -240,7 +240,6 @@ func pointCases() []pointCase { }, } for _, phase := range []string{PhaseStart, PhaseEnd, PhaseLoad, PhaseSave, PhaseRotate} { - phase := phase point := extension.InterceptorPoint("session." + phase) cases = append(cases, pointCase{ point: point, @@ -791,7 +790,7 @@ func TestOptionalTimeoutWarnsOnce(t *testing.T) { d := buildDispatcher( map[extension.InterceptorPoint][]extension.Contribution{point: {interceptor("opt", point, 0)}}, nil, map[string]*fakeClient{"opt": fake}, nil, warns) - for i := 0; i < 2; i++ { + for i := range 2 { payload := &ToolBeforePayload{Name: "bash", Arguments: `{"cmd":"ls"}`} result, err := d.Intercept(context.Background(), point, payload) if err != nil { @@ -990,7 +989,7 @@ func TestConcurrentDispatch(t *testing.T) { var wg sync.WaitGroup errs := make(chan error, 32) - for i := 0; i < 32; i++ { + for i := range 32 { wg.Add(1) go func(i int) { defer wg.Done() diff --git a/internal/extension/protocol/dto_test.go b/internal/extension/protocol/dto_test.go index ea9905c567..d03e8b51f8 100644 --- a/internal/extension/protocol/dto_test.go +++ b/internal/extension/protocol/dto_test.go @@ -319,11 +319,11 @@ func TestExternalizableFieldsAcceptNullPlaceholder(t *testing.T) { []byte(`{"surfaceId":"s","sessionId":"s","generation":0,"kind":"card","payload":null}`)); err == nil { t.Fatal("non-externalizable payload accepted null") } - pointers := ExternalizablePointers(reflect.TypeOf(InterceptParams{})) + pointers := ExternalizablePointers(reflect.TypeFor[InterceptParams]()) if !reflect.DeepEqual(pointers, []string{"/payload"}) { t.Fatalf("ExternalizablePointers(InterceptParams) = %v", pointers) } - pointers = ExternalizablePointers(reflect.TypeOf(ProviderRequest{})) + pointers = ExternalizablePointers(reflect.TypeFor[ProviderRequest]()) if !reflect.DeepEqual(pointers, []string{"/messages/*/content"}) { t.Fatalf("ExternalizablePointers(ProviderRequest) = %v", pointers) } diff --git a/internal/extension/protocol/dto_ui.go b/internal/extension/protocol/dto_ui.go index 4d586b5676..b123991c7c 100644 --- a/internal/extension/protocol/dto_ui.go +++ b/internal/extension/protocol/dto_ui.go @@ -147,13 +147,13 @@ func DecodeUIPublishPayload(kind UISurfaceKind, raw json.RawMessage) (any, error var typ reflect.Type switch kind { case UISurfaceStatus: - typ = reflect.TypeOf(UIStatusPayload{}) + typ = reflect.TypeFor[UIStatusPayload]() case UISurfaceCard: - typ = reflect.TypeOf(UICardPayload{}) + typ = reflect.TypeFor[UICardPayload]() case UISurfaceForm: - typ = reflect.TypeOf(UIFormPayload{}) + typ = reflect.TypeFor[UIFormPayload]() case UISurfaceNotification: - typ = reflect.TypeOf(UINotificationPayload{}) + typ = reflect.TypeFor[UINotificationPayload]() default: return nil, fmt.Errorf("protocol: unknown UI surface kind %q", kind) } @@ -167,7 +167,7 @@ func DecodeUIPublishPayload(kind UISurfaceKind, raw json.RawMessage) (any, error func DecodeUIRequestPayload(kind UIRequestKind, raw json.RawMessage) (any, error) { switch kind { case UIRequestConfirm, UIRequestInput, UIRequestSelect, UIRequestMultiselect: - return decodeAndValidate(raw, reflect.TypeOf(UIFormPayload{})) + return decodeAndValidate(raw, reflect.TypeFor[UIFormPayload]()) default: return nil, fmt.Errorf("protocol: unknown UI request kind %q", kind) } diff --git a/internal/extension/protocol/registry.go b/internal/extension/protocol/registry.go index f3288e2372..73a870dcc8 100644 --- a/internal/extension/protocol/registry.go +++ b/internal/extension/protocol/registry.go @@ -66,7 +66,7 @@ func extensionNotification[P any](name Method, class OperationClass) MethodSpec return MethodSpec{name, DirectionExtensionToHostNotification, class, typeOf[P](), typeOf[NoResult]()} } -func typeOf[T any]() reflect.Type { return reflect.TypeOf((*T)(nil)).Elem() } +func typeOf[T any]() reflect.Type { return reflect.TypeFor[T]() } // frozenRegistry is the Extension Protocol v1 method set. Adding, renaming, // or redirecting a method is a conscious protocol change: ValidateRegistry diff --git a/internal/extension/protocol/schema.go b/internal/extension/protocol/schema.go index 99c39aba66..0c258038a9 100644 --- a/internal/extension/protocol/schema.go +++ b/internal/extension/protocol/schema.go @@ -16,7 +16,7 @@ const SchemaDraft202012 = "https://json-schema.org/draft/2020-12/schema" // SchemaTitle is the generated document's human title. const SchemaTitle = "Reasonix Extension Protocol v1" -var rawMessageType = reflect.TypeOf(json.RawMessage{}) +var rawMessageType = reflect.TypeFor[json.RawMessage]() // BuildSchemaDocument reflection-walks the frozen registry and produces the // canonical JSON Schema (draft 2020-12) document: one methods object keyed by @@ -162,7 +162,7 @@ func buildJSONSchema(defs map[string]any, typ reflect.Type) (any, error) { func buildObjectSchema(defs map[string]any, typ reflect.Type) (map[string]any, error) { properties := map[string]any{} var required []string - for i := 0; i < typ.NumField(); i++ { + for i := range typ.NumField() { field := typ.Field(i) if field.PkgPath != "" { continue @@ -215,7 +215,7 @@ func applyFieldTags(schema any, field reflect.StructField) any { } return schema } - for _, tag := range strings.Split(field.Tag.Get("validate"), ",") { + for tag := range strings.SplitSeq(field.Tag.Get("validate"), ",") { switch { case tag == "nonempty": if object["type"] == "string" { diff --git a/internal/extension/protocol/validate.go b/internal/extension/protocol/validate.go index 94ccb5bb94..f93db770b9 100644 --- a/internal/extension/protocol/validate.go +++ b/internal/extension/protocol/validate.go @@ -8,6 +8,7 @@ import ( "io" "reflect" "regexp" + "slices" "sort" "strconv" "strings" @@ -29,19 +30,19 @@ var sha256Pattern = regexp.MustCompile(`^[0-9a-f]{64}$`) // The strict decoder rejects anything outside these sets; the schema // generator emits them as JSON Schema enums. var enumTypes = map[reflect.Type][]string{ - reflect.TypeOf(Direction("")): values(DirectionHostToExtensionRequest, DirectionExtensionToHostRequest, DirectionHostToExtensionNotification, DirectionExtensionToHostNotification), - reflect.TypeOf(OperationClass("")): values(ClassLifecycle, ClassIntercept, ClassObservation, ClassProvider, ClassUI, ClassContent), - reflect.TypeOf(InterceptEvent("")): interceptEventValues(), - reflect.TypeOf(InterceptDecision("")): values(DecisionContinue, DecisionBlock, DecisionReplace, DecisionAllow, DecisionDeny), - reflect.TypeOf(UIHostKind("")): values(UIHostTUI, UIHostDesktop, UIHostACP, UIHostHeadless), - reflect.TypeOf(UISurfaceKind("")): values(UISurfaceStatus, UISurfaceCard, UISurfaceForm, UISurfaceNotification), - reflect.TypeOf(UIRequestKind("")): values(UIRequestConfirm, UIRequestInput, UIRequestSelect, UIRequestMultiselect), - reflect.TypeOf(UIFieldKind("")): values(UIFieldConfirm, UIFieldInput, UIFieldSelect, UIFieldMultiselect), - reflect.TypeOf(UISeverity("")): values(UISeverityInfo, UISeverityWarn, UISeverityError), - reflect.TypeOf(ProviderRole("")): values(ProviderRoleSystem, ProviderRoleUser, ProviderRoleAssistant, ProviderRoleTool), - reflect.TypeOf(ProviderChunkType("")): values(ChunkText, ChunkReasoning, ChunkToolCallStart, ChunkToolCallDelta, ChunkToolCall, ChunkUsage, ChunkDone, ChunkError), - reflect.TypeOf(ProviderErrorCode("")): values(ProviderFailed, ProviderInterrupted), - reflect.TypeOf(ContentEncoding("")): values(ContentUTF8), + reflect.TypeFor[Direction](): values(DirectionHostToExtensionRequest, DirectionExtensionToHostRequest, DirectionHostToExtensionNotification, DirectionExtensionToHostNotification), + reflect.TypeFor[OperationClass](): values(ClassLifecycle, ClassIntercept, ClassObservation, ClassProvider, ClassUI, ClassContent), + reflect.TypeFor[InterceptEvent](): interceptEventValues(), + reflect.TypeFor[InterceptDecision](): values(DecisionContinue, DecisionBlock, DecisionReplace, DecisionAllow, DecisionDeny), + reflect.TypeFor[UIHostKind](): values(UIHostTUI, UIHostDesktop, UIHostACP, UIHostHeadless), + reflect.TypeFor[UISurfaceKind](): values(UISurfaceStatus, UISurfaceCard, UISurfaceForm, UISurfaceNotification), + reflect.TypeFor[UIRequestKind](): values(UIRequestConfirm, UIRequestInput, UIRequestSelect, UIRequestMultiselect), + reflect.TypeFor[UIFieldKind](): values(UIFieldConfirm, UIFieldInput, UIFieldSelect, UIFieldMultiselect), + reflect.TypeFor[UISeverity](): values(UISeverityInfo, UISeverityWarn, UISeverityError), + reflect.TypeFor[ProviderRole](): values(ProviderRoleSystem, ProviderRoleUser, ProviderRoleAssistant, ProviderRoleTool), + reflect.TypeFor[ProviderChunkType](): values(ChunkText, ChunkReasoning, ChunkToolCallStart, ChunkToolCallDelta, ChunkToolCall, ChunkUsage, ChunkDone, ChunkError), + reflect.TypeFor[ProviderErrorCode](): values(ProviderFailed, ProviderInterrupted), + reflect.TypeFor[ContentEncoding](): values(ContentUTF8), } func init() { @@ -50,7 +51,7 @@ func init() { for i := range contracts { reasons[i] = string(contracts[i].Reason) } - enumTypes[reflect.TypeOf(ErrorReason(""))] = reasons + enumTypes[reflect.TypeFor[ErrorReason]()] = reasons } // EnumValues returns the frozen wire values of every string enum DTO type, @@ -133,7 +134,7 @@ func validateRequiredJSON(raw json.RawMessage, typ reflect.Type, at string) erro } func validateRequiredObject(object map[string]json.RawMessage, typ reflect.Type, at string) error { - for i := 0; i < typ.NumField(); i++ { + for i := range typ.NumField() { field := typ.Field(i) if field.PkgPath != "" { continue @@ -172,7 +173,7 @@ func validateNestedRequired(raw json.RawMessage, typ reflect.Type, at string) er for typ.Kind() == reflect.Pointer { typ = typ.Elem() } - if typ == reflect.TypeOf(json.RawMessage{}) { + if typ == reflect.TypeFor[json.RawMessage]() { if len(bytes.TrimSpace(raw)) == 0 || !json.Valid(raw) { return validationError(at + " must contain valid JSON") } @@ -219,7 +220,7 @@ func validateValue(value reflect.Value, at string, omitEmpty bool) error { return validateValue(value.Elem(), at, false) } typ := value.Type() - if typ == reflect.TypeOf(json.RawMessage{}) { + if typ == reflect.TypeFor[json.RawMessage]() { raw := value.Interface().(json.RawMessage) if len(bytes.TrimSpace(raw)) == 0 { // An empty RawMessage is the zero value of an omitempty field and @@ -242,7 +243,7 @@ func validateValue(value reflect.Value, at string, omitEmpty bool) error { } switch value.Kind() { case reflect.Struct: - for i := 0; i < value.NumField(); i++ { + for i := range value.NumField() { field := typ.Field(i) if field.PkgPath != "" { continue @@ -277,7 +278,7 @@ func validateValue(value reflect.Value, at string, omitEmpty bool) error { } } case reflect.Slice, reflect.Array: - for i := 0; i < value.Len(); i++ { + for i := range value.Len() { if err := validateValue(value.Index(i), fmt.Sprintf("%s[%d]", at, i), false); err != nil { return err } @@ -309,7 +310,7 @@ func validateTag(value reflect.Value, tags, at string, omitEmpty bool) error { } value = value.Elem() } - for _, tag := range strings.Split(tags, ",") { + for tag := range strings.SplitSeq(tags, ",") { switch { case tag == "nonempty": if value.Kind() == reflect.String && strings.TrimSpace(value.String()) == "" { @@ -347,12 +348,7 @@ func numericValue(value reflect.Value) float64 { } func contains(items []string, value string) bool { - for _, item := range items { - if item == value { - return true - } - } - return false + return slices.Contains(items, value) } func jsonField(field reflect.StructField) (name string, omitEmpty, skip bool) { @@ -393,7 +389,7 @@ func collectExternalizablePointers(typ reflect.Type, prefix string, out *[]strin } switch typ.Kind() { case reflect.Struct: - for i := 0; i < typ.NumField(); i++ { + for i := range typ.NumField() { field := typ.Field(i) if field.PkgPath != "" { continue diff --git a/internal/extension/protocolgen/sdkgo.go b/internal/extension/protocolgen/sdkgo.go index d1bc89609e..ed1d64993b 100644 --- a/internal/extension/protocolgen/sdkgo.go +++ b/internal/extension/protocolgen/sdkgo.go @@ -47,7 +47,7 @@ var sdkInitialisms = map[string]string{ "ui": "UI", "tui": "TUI", "acp": "ACP", "utf8": "UTF8", } -var rawMessageType = reflect.TypeOf(json.RawMessage{}) +var rawMessageType = reflect.TypeFor[json.RawMessage]() // sdkTypeWalk is the deterministic first-visit record of every named wire // type reachable from the frozen registry (plus the extra roots below). @@ -77,11 +77,11 @@ func walkSDKTypes() (*sdkTypeWalk, error) { } } roots = append(roots, - reflect.TypeOf(protocol.UIStatusPayload{}), - reflect.TypeOf(protocol.UICardPayload{}), - reflect.TypeOf(protocol.UIFormPayload{}), - reflect.TypeOf(protocol.UINotificationPayload{}), - reflect.TypeOf(protocol.ProtocolErrorData{}), + reflect.TypeFor[protocol.UIStatusPayload](), + reflect.TypeFor[protocol.UICardPayload](), + reflect.TypeFor[protocol.UIFormPayload](), + reflect.TypeFor[protocol.UINotificationPayload](), + reflect.TypeFor[protocol.ProtocolErrorData](), ) for _, root := range roots { if err := w.visit(root); err != nil { @@ -109,7 +109,7 @@ func (w *sdkTypeWalk) visit(typ reflect.Type) error { w.seen[typ] = true w.order = append(w.order, typ) w.kinds[typ] = "struct" - for i := 0; i < typ.NumField(); i++ { + for i := range typ.NumField() { field := typ.Field(i) if field.PkgPath != "" { continue @@ -298,7 +298,7 @@ func emitSDKStruct(out *strings.Builder, typ reflect.Type) error { fmt.Fprintf(out, "// %s is a generated Extension Protocol v1 wire DTO.\n", typ.Name()) fields := 0 var body strings.Builder - for i := 0; i < typ.NumField(); i++ { + for i := range typ.NumField() { field := typ.Field(i) if field.PkgPath != "" { continue diff --git a/internal/extension/providerext/providerext_test.go b/internal/extension/providerext/providerext_test.go index 949df17889..7488f09c96 100644 --- a/internal/extension/providerext/providerext_test.go +++ b/internal/extension/providerext/providerext_test.go @@ -3,6 +3,7 @@ package providerext import ( "context" "errors" + "slices" "strings" "sync" "sync/atomic" @@ -107,11 +108,9 @@ func (f *fakeClient) waitCancel(t *testing.T, streamID string) { deadline := time.Now().Add(testBudget) for time.Now().Before(deadline) { f.mu.Lock() - for _, id := range f.cancels { - if id == streamID { - f.mu.Unlock() - return - } + if slices.Contains(f.cancels, streamID) { + f.mu.Unlock() + return } f.mu.Unlock() select { diff --git a/internal/extension/providerext/stream_test.go b/internal/extension/providerext/stream_test.go index 8b14b37eae..f862c47c4e 100644 --- a/internal/extension/providerext/stream_test.go +++ b/internal/extension/providerext/stream_test.go @@ -669,7 +669,7 @@ func TestConcurrentStreamsOnOneSidecar(t *testing.T) { id string } handles := make([]handle, 0, streamCount) - for i := 0; i < streamCount; i++ { + for i := range streamCount { p, err := r.Resolve(provider.Selection{Ref: "plugin/demo/fake/x"}) if err != nil { t.Fatalf("Resolve: %v", err) diff --git a/internal/extension/replace.go b/internal/extension/replace.go index a423c0360c..78492fd318 100644 --- a/internal/extension/replace.go +++ b/internal/extension/replace.go @@ -2,6 +2,7 @@ package extension import ( "fmt" + "maps" "strings" ) @@ -174,8 +175,6 @@ func (c *ReplaceClaims) Owner(slot Slot) (ContributionSource, bool) { // build state through the result. func (c *ReplaceClaims) Claims() map[Slot]ContributionSource { out := make(map[Slot]ContributionSource, len(c.owners)) - for slot, owner := range c.owners { - out[slot] = owner - } + maps.Copy(out, c.owners) return out } diff --git a/internal/extension/rpcwire/conn_strict_regression_test.go b/internal/extension/rpcwire/conn_strict_regression_test.go index 861c99a51c..3ed593f640 100644 --- a/internal/extension/rpcwire/conn_strict_regression_test.go +++ b/internal/extension/rpcwire/conn_strict_regression_test.go @@ -132,14 +132,14 @@ func TestInboundHandlerConcurrencyIsBoundedWithoutBlockingResponses(t *testing.T }) done := make(chan error, 1) go func() { done <- conn.Serve(context.Background()) }() - for i := 0; i < 2; i++ { + for range 2 { select { case <-started: case <-time.After(time.Second): t.Fatal("bounded handlers did not start") } } - for i := 0; i < 3; i++ { + for range 3 { select { case <-out.busy: case <-time.After(time.Second): @@ -169,7 +169,7 @@ func TestInboundHandlerConcurrencyIsBoundedWithoutBlockingResponses(t *testing.T func TestQueuedNotificationsPreserveBurstOrder(t *testing.T) { const count = 500 var input strings.Builder - for i := 0; i < count; i++ { + for i := range count { fmt.Fprintf(&input, "{\"jsonrpc\":\"2.0\",\"method\":\"note\",\"params\":{\"index\":%d}}\n", i) } conn := NewConn(strings.NewReader(input.String()), io.Discard, Options{ @@ -201,7 +201,7 @@ func TestQueuedNotificationsPreserveBurstOrder(t *testing.T) { func TestQueuedNotificationOverflowFailsConnection(t *testing.T) { var input strings.Builder - for i := 0; i < 100; i++ { + for i := range 100 { fmt.Fprintf(&input, "{\"jsonrpc\":\"2.0\",\"method\":\"note\",\"params\":{\"index\":%d}}\n", i) } conn := NewConn(strings.NewReader(input.String()), io.Discard, Options{ @@ -360,7 +360,7 @@ func TestConcurrentWritesDoNotInterleaveFrames(t *testing.T) { conn := NewConn(strings.NewReader(""), w, Options{}) var wg sync.WaitGroup errs := make(chan error, count) - for i := 0; i < count; i++ { + for i := range count { wg.Add(1) go func(i int) { defer wg.Done() diff --git a/internal/extension/rpcwire/conn_test.go b/internal/extension/rpcwire/conn_test.go index 6580a1df91..acc5f40160 100644 --- a/internal/extension/rpcwire/conn_test.go +++ b/internal/extension/rpcwire/conn_test.go @@ -88,7 +88,7 @@ func TestHandlerResponseAfterWriteReceivesTransportFailure(t *testing.T) { } func TestRequestKeepsDeliveredResponseWhenPeerClosesAfterWrite(t *testing.T) { - for attempt := 0; attempt < 100; attempt++ { + for attempt := range 100 { serverToClientR, serverToClientW := io.Pipe() clientToServerR, clientToServerW := io.Pipe() client := NewConn(serverToClientR, clientToServerW, Options{Name: "response-close-client"}) @@ -209,7 +209,7 @@ func TestTryNotifyDropsImmediatelyWhenQueueIsFull(t *testing.T) { } func TestWriterExitsAfterGracefulServeClose(t *testing.T) { - for attempt := 0; attempt < 100; attempt++ { + for attempt := range 100 { conn := NewConn(strings.NewReader(""), io.Discard, Options{Name: "writer-exit"}) if err := conn.Serve(context.Background()); err != nil { t.Fatalf("attempt %d Serve: %v", attempt, err) @@ -230,8 +230,7 @@ func TestRequestReturnsStructuredPeerError(t *testing.T) { server.Handle("fail", func(context.Context, json.RawMessage) (any, error) { return nil, &RPCError{Code: -32000, Message: "busy", Data: map[string]any{"reasonixCode": "HOST_BUSY"}} }) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := t.Context() go func() { _ = client.Serve(ctx) }() go func() { _ = server.Serve(ctx) }() _, err := client.Request(ctx, "fail", struct{}{}) @@ -267,7 +266,7 @@ func TestStrictJSONRPCRejectsMissingVersionAndInvalidShape(t *testing.T) { } dec := json.NewDecoder(&out) wantIDs := []string{"1", "2", "3", "null", "5", "null"} - for i := 0; i < 6; i++ { + for i := range 6 { var frame struct { ID json.RawMessage `json:"id"` Error *ErrorObject `json:"error"` diff --git a/internal/extension/rpcwire/stall_test.go b/internal/extension/rpcwire/stall_test.go index b1b5b38999..5324d54bff 100644 --- a/internal/extension/rpcwire/stall_test.go +++ b/internal/extension/rpcwire/stall_test.go @@ -81,7 +81,7 @@ func TestWriteCallerContextAbortLeavesConnectionAlive(t *testing.T) { wire := <-drained var ping bool - for _, line := range strings.Split(strings.TrimSpace(string(wire)), "\n") { + for line := range strings.SplitSeq(strings.TrimSpace(string(wire)), "\n") { if strings.TrimSpace(line) == "" { continue } @@ -162,8 +162,7 @@ func TestQueuedFrameCancelledBeforeStartNeverLands(t *testing.T) { func TestNotifyAfterGracefulCloseFails(t *testing.T) { serverToClientR, serverToClientW := io.Pipe() conn := NewConn(serverToClientR, io.Discard, Options{Name: "close-notify-test"}) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := t.Context() serveDone := make(chan error, 1) go func() { serveDone <- conn.Serve(ctx) }() @@ -176,7 +175,7 @@ func TestNotifyAfterGracefulCloseFails(t *testing.T) { t.Fatalf("Serve on graceful EOF: %v", err) } - for i := 0; i < 20; i++ { + for i := range 20 { if err := conn.Notify("after/close", nil); err == nil { t.Fatalf("attempt %d: post-close Notify reported success for a dropped frame", i) } diff --git a/internal/extension/runtimeset.go b/internal/extension/runtimeset.go index 9bb626d26b..34609f3ff7 100644 --- a/internal/extension/runtimeset.go +++ b/internal/extension/runtimeset.go @@ -3,6 +3,7 @@ package extension import ( "errors" "io" + "slices" "sync" ) @@ -73,8 +74,8 @@ func (s *RuntimeSet) Close() error { s.mu.Unlock() var errs []error - for i := len(closers) - 1; i >= 0; i-- { - if err := closers[i].Close(); err != nil { + for _, v := range slices.Backward(closers) { + if err := v.Close(); err != nil { errs = append(errs, err) } } diff --git a/internal/extension/sidecar/benchmark_test.go b/internal/extension/sidecar/benchmark_test.go index 7c93cf2ff7..8f8128ea5b 100644 --- a/internal/extension/sidecar/benchmark_test.go +++ b/internal/extension/sidecar/benchmark_test.go @@ -2,7 +2,7 @@ package sidecar import ( "context" - "sort" + "slices" "strconv" "testing" "time" @@ -47,7 +47,6 @@ func BenchmarkExtensionSidecarStartup(b *testing.B) { // latency instead of only the dispatcher's in-process overhead. func BenchmarkExtensionSidecarDispatchLatency(b *testing.B) { for _, count := range []int{1, 4} { - count := count b.Run("Turn/NoopSidecars"+strconv.Itoa(count), func(b *testing.B) { d := benchmarkSidecarDispatcher(b, extension.PointInputReceive, count) payload := dispatch.InputPayload{Text: "hello"} @@ -71,7 +70,7 @@ func benchmarkSidecarDispatcher(b *testing.B, point extension.InterceptorPoint, b.Helper() chain := make([]extension.Contribution, 0, count) clients := make(map[string]*Client, count) - for i := 0; i < count; i++ { + for i := range count { pluginID := "benchmark-" + strconv.Itoa(i) client := startFakeClient(b, func(rt *pluginpkg.RuntimeSpec) { rt.Intercepts = []string{string(point)} @@ -110,7 +109,7 @@ func benchmarkSidecarLatency(b *testing.B, fn func() error) { func reportSidecarPercentiles(b *testing.B, samples []int64) { b.Helper() - sort.Slice(samples, func(i, j int) bool { return samples[i] < samples[j] }) + slices.Sort(samples) if len(samples) == 0 { return } diff --git a/internal/extension/sidecar/client.go b/internal/extension/sidecar/client.go index 24f5814439..db0aec1547 100644 --- a/internal/extension/sidecar/client.go +++ b/internal/extension/sidecar/client.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "log/slog" + "slices" "strings" "sync" "sync/atomic" @@ -509,10 +510,7 @@ func (c *Client) Exited() bool { // context, and compaction family). func (c *Client) TimeoutFor(point extension.InterceptorPoint) time.Duration { if c.rt.TimeoutMillis > 0 { - timeout := time.Duration(c.rt.TimeoutMillis) * time.Millisecond - if timeout > maxInterceptTimeout { - timeout = maxInterceptTimeout - } + timeout := min(time.Duration(c.rt.TimeoutMillis)*time.Millisecond, maxInterceptTimeout) return timeout } switch point { @@ -790,10 +788,5 @@ func mapRequestError(err error) error { } func containsString(items []string, value string) bool { - for _, item := range items { - if item == value { - return true - } - } - return false + return slices.Contains(items, value) } diff --git a/internal/extension/sidecar/content.go b/internal/extension/sidecar/content.go index 8b39bfbbd4..7efc595b81 100644 --- a/internal/extension/sidecar/content.go +++ b/internal/extension/sidecar/content.go @@ -94,10 +94,7 @@ func (s *Store) Read(ref string, offset int64) (chunk []byte, next *int64, total if offset < 0 || offset > int64(len(object.data)) { return nil, nil, 0, "", protocol.MustProtocolError(protocol.ErrContentRefExpired) } - end := offset + protocol.ContentRefChunkBytes - if end > int64(len(object.data)) { - end = int64(len(object.data)) - } + end := min(offset+protocol.ContentRefChunkBytes, int64(len(object.data))) if end < int64(len(object.data)) { value := end next = &value diff --git a/internal/extension/sidecar/content_test.go b/internal/extension/sidecar/content_test.go index dc521dc865..1128f7ad16 100644 --- a/internal/extension/sidecar/content_test.go +++ b/internal/extension/sidecar/content_test.go @@ -147,8 +147,8 @@ func TestStoreEnforcesObjectCap(t *testing.T) { func TestStoreEvictsOldestBeyondCapacity(t *testing.T) { store := NewStore() refs := make([]string, 0, storeMaxEntries+1) - for i := 0; i < storeMaxEntries+1; i++ { - ref, _, _, err := store.Put([]byte(fmt.Sprintf("object-%03d", i))) + for i := range storeMaxEntries + 1 { + ref, _, _, err := store.Put(fmt.Appendf(nil, "object-%03d", i)) if err != nil { t.Fatalf("Put %d: %v", i, err) } diff --git a/internal/extension/sidecar/externalize.go b/internal/extension/sidecar/externalize.go index cfeab9915d..1b7d9be0b1 100644 --- a/internal/extension/sidecar/externalize.go +++ b/internal/extension/sidecar/externalize.go @@ -66,7 +66,7 @@ func (c *Client) externalizeInterceptParams(params *protocol.InterceptParams) er // externalizeEventParams applies the outbound content-ref rule to one event // notification's payload. func (c *Client) externalizeEventParams(params *protocol.EventParams) error { - pointer, err := externalizablePointer(reflect.TypeOf(*params), "/payload") + pointer, err := externalizablePointer(reflect.TypeFor[protocol.EventParams](), "/payload") if err != nil { return err } diff --git a/internal/extension/sidecar/fake_test.go b/internal/extension/sidecar/fake_test.go index 84ec568c16..7b1ae5f9f2 100644 --- a/internal/extension/sidecar/fake_test.go +++ b/internal/extension/sidecar/fake_test.go @@ -67,7 +67,7 @@ type fakeExternalizedField struct { func fakeModes() map[string]bool { out := map[string]bool{} - for _, mode := range strings.Split(os.Getenv(fakeEnvMode), ",") { + for mode := range strings.SplitSeq(os.Getenv(fakeEnvMode), ",") { if mode = strings.TrimSpace(mode); mode != "" { out[mode] = true } @@ -93,7 +93,7 @@ func runFakeSidecar(stdin io.Reader, stdout io.Writer) { // Push well past the 16 KiB tail so only the end survives, then leave // a credential-looking line at the very end — it must be retained by // the ring and redacted before surfacing. - for i := 0; i < 32*1024; i++ { + for i := range 32 * 1024 { fmt.Fprintf(os.Stderr, "flood line %d padding padding padding\n", i) } fmt.Fprintln(os.Stderr, "boot failed: api_key=sk-abcdef1234567890SECRETKEY is invalid") diff --git a/internal/extension/sidecar/manager_test.go b/internal/extension/sidecar/manager_test.go index a5ea18f21c..894e9ade67 100644 --- a/internal/extension/sidecar/manager_test.go +++ b/internal/extension/sidecar/manager_test.go @@ -211,7 +211,7 @@ func TestStartLoadedPackagesBoundsParallelismAndSharesCancellation(t *testing.T) done <- startResult{manager: manager, warnings: warnings, err: err} }() - for i := 0; i < maxConcurrentPackageStarts; i++ { + for i := range maxConcurrentPackageStarts { select { case <-entered: case <-time.After(5 * time.Second): diff --git a/internal/extension/sidecar/process.go b/internal/extension/sidecar/process.go index 694d2e9ba6..7379121eb0 100644 --- a/internal/extension/sidecar/process.go +++ b/internal/extension/sidecar/process.go @@ -85,10 +85,7 @@ func newStartupFailure(stage string, started time.Time, stderr string, err error if errors.As(err, &existing) { return err } - elapsed := time.Since(started) - if elapsed < 0 { - elapsed = 0 - } + elapsed := max(time.Since(started), 0) return &startupFailure{ Stage: strings.TrimSpace(stage), Elapsed: elapsed, diff --git a/internal/extension/snapshot.go b/internal/extension/snapshot.go index ba0be9ed4e..bb9d960976 100644 --- a/internal/extension/snapshot.go +++ b/internal/extension/snapshot.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "maps" "sort" "reasonix/internal/provider" @@ -64,9 +65,7 @@ func (s *RuntimeSnapshot) InterceptorChain() map[InterceptorPoint][]Contribution // Replacements returns the winning owner per replacement slot (a copy). func (s *RuntimeSnapshot) Replacements() map[Slot]ContributionSource { out := make(map[Slot]ContributionSource, len(s.replacements)) - for slot, owner := range s.replacements { - out[slot] = owner - } + maps.Copy(out, s.replacements) return out } diff --git a/internal/extension/uihub/hub_test.go b/internal/extension/uihub/hub_test.go index 49510f44ed..24d8af8b21 100644 --- a/internal/extension/uihub/hub_test.go +++ b/internal/extension/uihub/hub_test.go @@ -660,7 +660,7 @@ func TestHubConcurrentUse(t *testing.T) { t.Fatal(err) } var wg sync.WaitGroup - for i := 0; i < 8; i++ { + for i := range 8 { wg.Add(1) go func(i int) { defer wg.Done() diff --git a/internal/fileref/search.go b/internal/fileref/search.go index ab7d88e6b5..8adf246a19 100644 --- a/internal/fileref/search.go +++ b/internal/fileref/search.go @@ -149,10 +149,7 @@ func Search(root, query string, limit int) []SearchResult { // out by a large number of file matches. const dirQuota = 5 out := make([]SearchResult, 0, limit) - nDirs := len(dirHits) - if nDirs > dirQuota { - nDirs = dirQuota - } + nDirs := min(len(dirHits), dirQuota) out = append(out, dirHits[:nDirs]...) remaining := limit - len(out) if remaining > 0 { @@ -177,7 +174,7 @@ func Search(root, query string, limit int) []SearchResult { // directories above the file (e.g. "src/planind/index.tsx" with query // "planind" matches the "planind" segment). func pathSegmentContains(relSlash, queryLower string) bool { - for _, seg := range strings.Split(relSlash, "/") { + for seg := range strings.SplitSeq(relSlash, "/") { if strings.Contains(strings.ToLower(seg), queryLower) { return true } diff --git a/internal/fileutil/encoding/encoding.go b/internal/fileutil/encoding/encoding.go index 919ccfa35c..1e68df5a82 100644 --- a/internal/fileutil/encoding/encoding.go +++ b/internal/fileutil/encoding/encoding.go @@ -99,7 +99,7 @@ func DetectUTF16NoBOM(b []byte) (Kind, bool) { } n &^= 1 // examine an even-length window so parity counts are comparable var evenNUL, oddNUL int - for i := 0; i < n; i++ { + for i := range n { if b[i] != 0 { continue } diff --git a/internal/fileutil/globset.go b/internal/fileutil/globset.go index ddb002d7b9..1fdc6a4028 100644 --- a/internal/fileutil/globset.go +++ b/internal/fileutil/globset.go @@ -54,8 +54,8 @@ func MatchSlashGlob(path, pattern string) bool { if matched, _ := doublestar.Match(pattern, path); matched { return true } - if strings.HasPrefix(pattern, "**/") { - matched, _ := doublestar.Match(strings.TrimPrefix(pattern, "**/"), path) + if after, ok := strings.CutPrefix(pattern, "**/"); ok { + matched, _ := doublestar.Match(after, path) return matched } return false diff --git a/internal/guardian/guardian.go b/internal/guardian/guardian.go index fef31113bf..0ebff756d1 100644 --- a/internal/guardian/guardian.go +++ b/internal/guardian/guardian.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "os" + "slices" "strings" "sync" "time" @@ -557,9 +558,9 @@ func firstRunesStr(s string, n int) string { func lastAssistantText(sess *agent.Session) string { msgs := sess.Snapshot() - for i := len(msgs) - 1; i >= 0; i-- { - if msgs[i].Role == provider.RoleAssistant && strings.TrimSpace(msgs[i].Content) != "" { - return msgs[i].Content + for _, v := range slices.Backward(msgs) { + if v.Role == provider.RoleAssistant && strings.TrimSpace(v.Content) != "" { + return v.Content } } return "" diff --git a/internal/guardian/guardian_test.go b/internal/guardian/guardian_test.go index 35d9f655a5..32bc086df6 100644 --- a/internal/guardian/guardian_test.go +++ b/internal/guardian/guardian_test.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "strings" "sync" "testing" @@ -108,7 +109,7 @@ func TestParseAssessmentRejectsUnknownEnum(t *testing.T) { func TestTranscriptRenderKeepsFirstAndLastUserAnchors(t *testing.T) { entries := []TranscriptEntry{{Kind: "user", Text: "first task"}} - for i := 0; i < maxRecentEntries+5; i++ { + for range maxRecentEntries + 5 { entries = append(entries, TranscriptEntry{Kind: "assistant", Text: "assistant detail"}) } entries = append(entries, TranscriptEntry{Kind: "user", Text: "latest instruction"}) @@ -249,7 +250,7 @@ func TestGuardianReviewTurnsAlternateRoles(t *testing.T) { parent := agent.NewSession("sys") parent.Add(provider.Message{Role: provider.RoleUser, Content: "do the thing"}) - for i := 0; i < 2; i++ { + for i := range 2 { if allow, _, err := gs.Review(context.Background(), "write_file", json.RawMessage(`{"file_path":"a.txt"}`), parent); err != nil || !allow { t.Fatalf("review %d = allow %v err %v, want allow nil", i+1, allow, err) } @@ -270,9 +271,9 @@ func TestGuardianReviewTurnsAlternateRoles(t *testing.T) { // The combined message must still carry the evidence boundary and the action. msgs := reqs[len(reqs)-1].Messages var review string - for i := len(msgs) - 1; i >= 0; i-- { - if msgs[i].Role == provider.RoleUser { - review = msgs[i].Content + for _, v := range slices.Backward(msgs) { + if v.Role == provider.RoleUser { + review = v.Content break } } @@ -355,7 +356,7 @@ func TestGuardianSessionAlternatesAfterCompaction(t *testing.T) { parent := agent.NewSession("sys") filler := strings.Repeat("parent transcript filler. ", 160) - for i := 0; i < compactEvery; i++ { + for i := range compactEvery { parent.Add(provider.Message{Role: provider.RoleUser, Content: fmt.Sprintf("turn %d: %s", i, filler)}) if allow, _, err := gs.Review(context.Background(), "write_file", json.RawMessage(`{"file_path":"a.txt"}`), parent); err != nil || !allow { t.Fatalf("review %d = allow %v err %v, want allow nil", i+1, allow, err) diff --git a/internal/guardian/transcript.go b/internal/guardian/transcript.go index 7e7184bc01..c83bdc0ac4 100644 --- a/internal/guardian/transcript.go +++ b/internal/guardian/transcript.go @@ -2,6 +2,7 @@ package guardian import ( "fmt" + "slices" "strings" "unicode/utf8" @@ -127,8 +128,8 @@ func renderTranscript(entries []TranscriptEntry) ([]string, string) { } // Fill remaining message budget with user entries from newest to oldest. - for i := len(userIdx) - 1; i >= 0; i-- { - idx := userIdx[i] + for _, v := range slices.Backward(userIdx) { + idx := v if idx >= len(all) || included[idx] { continue } @@ -226,17 +227,8 @@ func truncateText(content string, tokCap int) (string, bool) { runes := []rune(content) // Estimate how many runes fit in head/tail bytes (conservative: assume // max 4 bytes per rune). - headRunes := head / 4 - if headRunes > len(runes) { - headRunes = len(runes) - } - tailRunes := tail / 4 - if tailRunes > len(runes)-headRunes { - tailRunes = len(runes) - headRunes - } - if tailRunes < 0 { - tailRunes = 0 - } + headRunes := min(head/4, len(runes)) + tailRunes := max(min(tail/4, len(runes)-headRunes), 0) return string(runes[:headRunes]) + marker + string(runes[len(runes)-tailRunes:]), true } diff --git a/internal/history/search.go b/internal/history/search.go index da41debf96..31dbb39ec4 100644 --- a/internal/history/search.go +++ b/internal/history/search.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "maps" "os" "path/filepath" "sort" @@ -247,10 +248,7 @@ func (s *Searcher) Around(ctx context.Context, req AroundRequest) ([]MessageCont } before := clamp(req.Before, defaultAround, maxAround) after := clamp(req.After, defaultAround, maxAround) - start := req.MessageIndex - before - if start < 0 { - start = 0 - } + start := max(req.MessageIndex-before, 0) remainingAfter := len(msgs) - req.MessageIndex - 1 end := len(msgs) if after < remainingAfter { @@ -277,9 +275,7 @@ func normalizeScope(scope string) (string, error) { func normalizeKinds(kinds []Kind) (map[Kind]bool, error) { if len(kinds) == 0 { out := make(map[Kind]bool, len(defaultKinds)) - for k, v := range defaultKinds { - out[k] = v - } + maps.Copy(out, defaultKinds) return out, nil } out := map[Kind]bool{} diff --git a/internal/history/search_test.go b/internal/history/search_test.go index 1211e36007..30c47f0295 100644 --- a/internal/history/search_test.go +++ b/internal/history/search_test.go @@ -131,7 +131,7 @@ func TestSearchDropsCommonWordNoise(t *testing.T) { writeSession(t, filepath.Join(sessionDir, "rare.jsonl"), []provider.Message{ {Role: provider.RoleUser, Content: "rareterm common common common"}, }) - for i := 0; i < 12; i++ { + for i := range 12 { writeSession(t, filepath.Join(sessionDir, "common-"+string(rune('a'+i))+".jsonl"), []provider.Message{ {Role: provider.RoleUser, Content: "common"}, }) diff --git a/internal/hook/hook.go b/internal/hook/hook.go index 25d573c264..c04ac22cf5 100644 --- a/internal/hook/hook.go +++ b/internal/hook/hook.go @@ -23,6 +23,7 @@ import ( "path/filepath" "regexp" "runtime" + "slices" "sort" "strings" "time" @@ -483,12 +484,7 @@ func isShellVariableNameByte(c byte) bool { } func validEvent(event Event) bool { - for _, e := range Events { - if e == event { - return true - } - } - return false + return slices.Contains(Events, event) } func cloneEnv(in map[string]string) map[string]string { @@ -519,12 +515,7 @@ func MatchesTool(h ResolvedHook, toolName string) bool { if h.PayloadFormat != "claude" { return re.MatchString(toolName) } - for _, candidate := range claudeMatchNames(toolName) { - if re.MatchString(candidate) { - return true - } - } - return false + return slices.ContainsFunc(claudeMatchNames(toolName), re.MatchString) } // claudeAgentSpawningTools are every Reasonix tool that spawns a subagent and diff --git a/internal/hook/inspect.go b/internal/hook/inspect.go index 7aba66546b..dfbe26b9d3 100644 --- a/internal/hook/inspect.go +++ b/internal/hook/inspect.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "regexp" + "slices" "sort" "strings" @@ -162,7 +163,7 @@ func appendInspectEntries(out *Inspection, s *Settings, scope Scope, source stri unknown = append(unknown, event) } } - sort.Slice(unknown, func(i, j int) bool { return unknown[i] < unknown[j] }) + slices.Sort(unknown) for _, event := range unknown { for _, cfg := range s.Hooks[event] { out.Entries = append(out.Entries, Entry{ diff --git a/internal/hook/windows_compat.go b/internal/hook/windows_compat.go index b319902c54..3b6562fe1f 100644 --- a/internal/hook/windows_compat.go +++ b/internal/hook/windows_compat.go @@ -123,7 +123,7 @@ func isWindowsBatchExecutable(executable string) bool { func isSimpleWindowsBatchTail(tail string) bool { quoted := false - for i := 0; i < len(tail); i++ { + for i := range len(tail) { switch tail[i] { case '\r', '\n': return false @@ -166,8 +166,8 @@ func hasCommandStringFlag(args []string) bool { if arg == "-" || arg == "--" || !strings.HasPrefix(arg, "-") { return false } - if strings.HasPrefix(arg, "--") { - name, _, hasInlineValue := strings.Cut(strings.TrimPrefix(arg, "--"), "=") + if after, ok := strings.CutPrefix(arg, "--"); ok { + name, _, hasInlineValue := strings.Cut(after, "=") if !hasInlineValue && bashLongOptionNeedsOperand(name) { if i+1 >= len(args) { return false diff --git a/internal/i18n/catalog_parity_test.go b/internal/i18n/catalog_parity_test.go index 765051a244..5ddd524bf6 100644 --- a/internal/i18n/catalog_parity_test.go +++ b/internal/i18n/catalog_parity_test.go @@ -35,7 +35,7 @@ func TestCatalogsAgreeOnCodeTokens(t *testing.T) { en := reflect.ValueOf(English) typ := en.Type() - for i := 0; i < typ.NumField(); i++ { + for i := range typ.NumField() { name := typ.Field(i).Name if excluded[name] { continue diff --git a/internal/i18n/i18n_test.go b/internal/i18n/i18n_test.go index 3dced00bf2..57363f0f59 100644 --- a/internal/i18n/i18n_test.go +++ b/internal/i18n/i18n_test.go @@ -15,7 +15,7 @@ func TestCatalogsComplete(t *testing.T) { typ := en.Type() catalogs := map[string]reflect.Value{"zh": reflect.ValueOf(Chinese), "zh-TW": reflect.ValueOf(ChineseTraditional)} for tag, cat := range catalogs { - for i := 0; i < typ.NumField(); i++ { + for i := range typ.NumField() { name := typ.Field(i).Name if strings.TrimSpace(cat.Field(i).String()) == "" { t.Errorf("%s catalogue: field %q is empty", tag, name) @@ -31,7 +31,7 @@ func TestCatalogsComplete(t *testing.T) { func TestCatalogsAgreeOnPlaceholders(t *testing.T) { en := reflect.ValueOf(English) typ := en.Type() - for i := 0; i < typ.NumField(); i++ { + for i := range typ.NumField() { name := typ.Field(i).Name if !strings.HasSuffix(name, "Fmt") { continue @@ -61,7 +61,7 @@ func TestPlanApprovalChoicesExposeThreeExplicitActions(t *testing.T) { for _, tt := range tests { t.Run(tt.tag, func(t *testing.T) { numbered := 0 - for _, line := range strings.Split(tt.value, "\n") { + for line := range strings.SplitSeq(tt.value, "\n") { line = strings.TrimSpace(line) if len(line) >= 3 && line[0] >= '1' && line[0] <= '9' && line[1] == '.' { numbered++ diff --git a/internal/installlayout/activate.go b/internal/installlayout/activate.go index 85089f1be0..af07995484 100644 --- a/internal/installlayout/activate.go +++ b/internal/installlayout/activate.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" "runtime" + "slices" "strings" "time" @@ -239,8 +240,8 @@ func publishRootEntries(installRoot, stagingRoot string, members []Member) (roll replacements := make([]replacement, 0, len(members)) rollbackFn := func() error { var rollbackErr error - for i := len(replacements) - 1; i >= 0; i-- { - r := replacements[i] + for _, v := range slices.Backward(replacements) { + r := v if err := os.Remove(r.destination); err != nil && !os.IsNotExist(err) { rollbackErr = errors.Join(rollbackErr, err) } diff --git a/internal/installlayout/current.go b/internal/installlayout/current.go index 09e1f5feb3..a02240a694 100644 --- a/internal/installlayout/current.go +++ b/internal/installlayout/current.go @@ -209,7 +209,7 @@ func ValidateActiveDir(installRoot, version, activeDir string) error { func rejectSymlinkPathComponents(root, rel string) error { cur := root - for _, part := range strings.Split(rel, string(os.PathSeparator)) { + for part := range strings.SplitSeq(rel, string(os.PathSeparator)) { if part == "" || part == "." { continue } diff --git a/internal/installsource/apply.go b/internal/installsource/apply.go index 30b08ef307..8d1c816ad6 100644 --- a/internal/installsource/apply.go +++ b/internal/installsource/apply.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "reflect" + "slices" "strings" "reasonix/internal/config" @@ -84,11 +85,8 @@ func (t *installSourceTool) applySkillRoot(req request, act *action) error { } } for _, listed := range store.List() { - for _, name := range act.Skills { - if listed.Name == name { - act.Indexed = true - break - } + if slices.Contains(act.Skills, listed.Name) { + act.Indexed = true } } act.Target = act.Source diff --git a/internal/installsource/plan.go b/internal/installsource/plan.go index 8523924308..6f3170a7c2 100644 --- a/internal/installsource/plan.go +++ b/internal/installsource/plan.go @@ -19,8 +19,8 @@ var githubAPIBaseURL = "https://api.github.com" // plan turns a request into a list of actions plus a warnings slice. It // does not touch the disk; the apply phase is responsible for side effects. func (t *installSourceTool) plan(ctx context.Context, req request) ([]action, []string, error) { - if strings.HasPrefix(req.Source, "git:github.com/") { - req.Source = "https://github.com/" + strings.TrimPrefix(req.Source, "git:github.com/") + if after, ok := strings.CutPrefix(req.Source, "git:github.com/"); ok { + req.Source = "https://github.com/" + after } if isURL(req.Source) { return t.planURL(ctx, req) diff --git a/internal/installsource/plugin_package.go b/internal/installsource/plugin_package.go index 369300bf79..6310bb1fbe 100644 --- a/internal/installsource/plugin_package.go +++ b/internal/installsource/plugin_package.go @@ -500,8 +500,8 @@ func (t *installSourceTool) applyInstallPluginPackage(ctx context.Context, req r func (t *installSourceTool) preparePluginSource(ctx context.Context, source, mode string) (string, string, func(), error) { source = strings.TrimSpace(source) - if strings.HasPrefix(source, "git:github.com/") { - source = "https://github.com/" + strings.TrimPrefix(source, "git:github.com/") + if after, ok := strings.CutPrefix(source, "git:github.com/"); ok { + source = "https://github.com/" + after } if isURL(source) { src, ok := parseGitHubRepoSource(source) diff --git a/internal/instruction/resolver.go b/internal/instruction/resolver.go index 9368872631..4696a830f9 100644 --- a/internal/instruction/resolver.go +++ b/internal/instruction/resolver.go @@ -347,7 +347,7 @@ func directoryChain(root, target string) []string { } chain := []string{root} current := root - for _, part := range strings.Split(rel, string(filepath.Separator)) { + for part := range strings.SplitSeq(rel, string(filepath.Separator)) { if part == "" || part == "." { continue } diff --git a/internal/jobs/concurrency_stress_test.go b/internal/jobs/concurrency_stress_test.go index b8dde58c9c..151cebf795 100644 --- a/internal/jobs/concurrency_stress_test.go +++ b/internal/jobs/concurrency_stress_test.go @@ -19,10 +19,10 @@ func TestManagerConcurrentAccess(t *testing.T) { const workers = 24 var wg sync.WaitGroup wg.Add(workers) - for w := 0; w < workers; w++ { + for w := range workers { go func(w int) { defer wg.Done() - for i := 0; i < 200; i++ { + for i := range 200 { switch (w + i) % 6 { case 0: j := m.Start("bash", "x", func(ctx context.Context, out io.Writer) (string, error) { diff --git a/internal/jobs/jobs.go b/internal/jobs/jobs.go index 5921704725..bfe75fbfd1 100644 --- a/internal/jobs/jobs.go +++ b/internal/jobs/jobs.go @@ -20,6 +20,7 @@ import ( "os" "path/filepath" "runtime/debug" + "slices" "sort" "strings" "sync" @@ -1452,9 +1453,9 @@ func rebaseArtifactMigrationJobs(jobs []artifactMigrationJob, dir string) { } func unlockArtifactMigrationJobs(jobs []artifactMigrationJob) { - for i := len(jobs) - 1; i >= 0; i-- { - if jobs[i].job != nil { - jobs[i].job.mu.Unlock() + for _, v := range slices.Backward(jobs) { + if v.job != nil { + v.job.mu.Unlock() } } } diff --git a/internal/jobs/jobs_test.go b/internal/jobs/jobs_test.go index 8b25a0e376..fc1d8c15ba 100644 --- a/internal/jobs/jobs_test.go +++ b/internal/jobs/jobs_test.go @@ -120,7 +120,7 @@ func TestReserveStartForSessionIsAtomic(t *testing.T) { start := make(chan struct{}) releases := make(chan func(), callers) results := make(chan bool, callers) - for i := 0; i < callers; i++ { + for range callers { go func() { <-start release, _, ok := m.ReserveStartForSession("session-a", "task", 3) @@ -132,7 +132,7 @@ func TestReserveStartForSessionIsAtomic(t *testing.T) { } close(start) reserved := 0 - for i := 0; i < callers; i++ { + for range callers { if <-results { reserved++ } @@ -140,7 +140,7 @@ func TestReserveStartForSessionIsAtomic(t *testing.T) { if reserved != 3 { t.Fatalf("concurrent reservations = %d, want exactly 3", reserved) } - for i := 0; i < reserved; i++ { + for range reserved { (<-releases)() } if release, running, ok := m.ReserveStartForSession("session-a", "task", 3); !ok || running != 0 { diff --git a/internal/lsp/position.go b/internal/lsp/position.go index a480cc45bf..a4c6401157 100644 --- a/internal/lsp/position.go +++ b/internal/lsp/position.go @@ -58,11 +58,11 @@ func locate(content string, line1 int, symbol, enc string) (Position, error) { return Position{}, fmt.Errorf("line %d out of range (file has %d lines)", line1, len(lines)) } text := strings.TrimSuffix(lines[line1-1], "\r") - col := strings.Index(text, symbol) - if col < 0 { + before, _, ok := strings.Cut(text, symbol) + if !ok { return Position{}, fmt.Errorf("symbol %q not found on line %d", symbol, line1) } - return Position{Line: line1 - 1, Character: encodeChar(text[:col], enc)}, nil + return Position{Line: line1 - 1, Character: encodeChar(before, enc)}, nil } func encodeChar(prefix, enc string) int { diff --git a/internal/mcplaunch/launch.go b/internal/mcplaunch/launch.go index 88e8a44804..c3277177ef 100644 --- a/internal/mcplaunch/launch.go +++ b/internal/mcplaunch/launch.go @@ -424,7 +424,7 @@ func acquireFileLock(path string, wait time.Duration) (func(), error) { if _, err := rand.Read(token); err != nil { return nil, fmt.Errorf("generate MCP authorization lock owner: %w", err) } - owner := []byte(fmt.Sprintf("%d %s\n", os.Getpid(), hex.EncodeToString(token))) + owner := fmt.Appendf(nil, "%d %s\n", os.Getpid(), hex.EncodeToString(token)) deadline := time.Now().Add(wait) for { if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { diff --git a/internal/memory/auto_recall.go b/internal/memory/auto_recall.go index 76dcb8639b..a13a8de238 100644 --- a/internal/memory/auto_recall.go +++ b/internal/memory/auto_recall.go @@ -296,7 +296,7 @@ func autoRecallSearchText(memory Memory) string { } func distinctiveQueryTerm(query, normalizedTerm string) bool { - for _, field := range strings.Fields(query) { + for field := range strings.FieldsSeq(query) { trimmed := strings.Trim(field, "#()[]{}<>,;:'\"`!?=+*/\\|") if !strings.EqualFold(trimmed, normalizedTerm) { continue @@ -464,10 +464,7 @@ func clippedRecallEntry(hit RecallHit, maxRunes int) string { if utf8.RuneCountInString(entry) <= maxRunes { return entry } - cut := len(runes) / 4 - if cut < 1 { - cut = 1 - } + cut := max(len(runes)/4, 1) runes = runes[:len(runes)-cut] } return "" diff --git a/internal/memory/recall_test.go b/internal/memory/recall_test.go index 49f4ce74f6..2951d7939d 100644 --- a/internal/memory/recall_test.go +++ b/internal/memory/recall_test.go @@ -73,7 +73,7 @@ func TestRecallToolDropsCommonWordNoise(t *testing.T) { Type: TypeProject, Body: "rareterm common common common", }) - for i := 0; i < 12; i++ { + for i := range 12 { saveMemory(t, store, Memory{ Name: "common-note-" + string(rune('a'+i)), Description: "Common note", diff --git a/internal/memory/store.go b/internal/memory/store.go index f2e1db9228..5c6ee08392 100644 --- a/internal/memory/store.go +++ b/internal/memory/store.go @@ -462,7 +462,7 @@ var indexLineRe = regexp.MustCompile(`(?m)^\s*-\s\[.+?\]\(([^)]+)\.md\)\s*—\s. func indexLinesExceptIn(dir, name string) map[string]string { existing, _ := fileencoding.ReadFileUTF8(filepath.Join(dir, indexFile)) keep := map[string]string{} - for _, line := range strings.Split(string(existing), "\n") { + for line := range strings.SplitSeq(string(existing), "\n") { if mt := indexLineRe.FindStringSubmatch(line); mt != nil && mt[1] != name { keep[mt[1]] = strings.TrimRight(line, "\r") } @@ -475,7 +475,7 @@ func indexContainsIn(dir, name string) bool { if err != nil { return false } - for _, line := range strings.Split(string(existing), "\n") { + for line := range strings.SplitSeq(string(existing), "\n") { if mt := indexLineRe.FindStringSubmatch(line); mt != nil && mt[1] == name { return true } @@ -492,7 +492,7 @@ func flushIndexIn(dir string, lines map[string]string) error { processed := map[string]bool{} var preserved strings.Builder preservedEmpty := true - for _, line := range strings.Split(string(existing), "\n") { + for line := range strings.SplitSeq(string(existing), "\n") { trimmed := strings.TrimRight(line, "\r") if mt := indexLineRe.FindStringSubmatch(trimmed); mt != nil { name := mt[1] diff --git a/internal/memory/store_v2.go b/internal/memory/store_v2.go index 3981e7f2f7..7d2dd114b4 100644 --- a/internal/memory/store_v2.go +++ b/internal/memory/store_v2.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "sort" "strconv" "strings" @@ -240,8 +241,8 @@ func (s Store) findActive(ref string) (Memory, string, bool) { if parsed.qualified { return s.findActiveInDir(s.DirFor(parsed.scope), parsed.raw) } - for i := len(s.dirs()) - 1; i >= 0; i-- { - dir := s.dirs()[i] + for _, v := range slices.Backward(s.dirs()) { + dir := v if memory, path, ok := s.findActiveInDir(dir, parsed.raw); ok { return memory, path, true } diff --git a/internal/memory/store_v2_test.go b/internal/memory/store_v2_test.go index 237d1a29a6..3b0004d052 100644 --- a/internal/memory/store_v2_test.go +++ b/internal/memory/store_v2_test.go @@ -339,12 +339,10 @@ func TestStoreV2RestoreArchivedIsConcurrencySafe(t *testing.T) { var wg sync.WaitGroup errs := make(chan error, 2) for range 2 { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { _, restoreErr := store.RestoreArchived(archivePath) errs <- restoreErr - }() + }) } wg.Wait() close(errs) diff --git a/internal/netclient/netclient_test.go b/internal/netclient/netclient_test.go index a9c5732177..9850803e38 100644 --- a/internal/netclient/netclient_test.go +++ b/internal/netclient/netclient_test.go @@ -125,17 +125,17 @@ func TestSummaryRedactsPassword(t *testing.T) { } func TestHTTPClientProxyModesAffectRequests(t *testing.T) { - var targetHits int32 + var targetHits atomic.Int32 target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - atomic.AddInt32(&targetHits, 1) + targetHits.Add(1) _, _ = io.WriteString(w, "target") })) t.Cleanup(target.Close) targetAddr := strings.TrimPrefix(target.URL, "http://") - var envProxyHits int32 + var envProxyHits atomic.Int32 envProxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - atomic.AddInt32(&envProxyHits, 1) + envProxyHits.Add(1) if got, want := r.URL.String(), "http://service.test/resource"; got != want { t.Errorf("env proxy request URL = %q, want %q", got, want) } @@ -143,9 +143,9 @@ func TestHTTPClientProxyModesAffectRequests(t *testing.T) { })) t.Cleanup(envProxy.Close) - var customProxyHits int32 + var customProxyHits atomic.Int32 customProxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - atomic.AddInt32(&customProxyHits, 1) + customProxyHits.Add(1) if got, want := r.URL.String(), "http://service.test/resource"; got != want { t.Errorf("custom proxy request URL = %q, want %q", got, want) } @@ -204,9 +204,9 @@ func TestHTTPClientProxyModesAffectRequests(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - atomic.StoreInt32(&targetHits, 0) - atomic.StoreInt32(&envProxyHits, 0) - atomic.StoreInt32(&customProxyHits, 0) + targetHits.Store(0) + envProxyHits.Store(0) + customProxyHits.Store(0) client := mappedClient(t, tt.spec, "service.test:80", targetAddr) resp, err := client.Get("http://service.test/resource") @@ -221,13 +221,13 @@ func TestHTTPClientProxyModesAffectRequests(t *testing.T) { if string(body) != tt.wantBody { t.Fatalf("body = %q, want %q", body, tt.wantBody) } - if got := atomic.LoadInt32(&targetHits); got != tt.wantTargetHits { + if got := targetHits.Load(); got != tt.wantTargetHits { t.Fatalf("target hits = %d, want %d", got, tt.wantTargetHits) } - if got := atomic.LoadInt32(&envProxyHits); got != tt.wantEnvHits { + if got := envProxyHits.Load(); got != tt.wantEnvHits { t.Fatalf("env proxy hits = %d, want %d", got, tt.wantEnvHits) } - if got := atomic.LoadInt32(&customProxyHits); got != tt.wantCustomHits { + if got := customProxyHits.Load(); got != tt.wantCustomHits { t.Fatalf("custom proxy hits = %d, want %d", got, tt.wantCustomHits) } }) @@ -306,18 +306,18 @@ func TestStructuredProxyTypesAffectRequests(t *testing.T) { }) } - if got := atomic.LoadInt32(&socks5Proxy.hits); got != 1 { + if got := socks5Proxy.hits.Load(); got != 1 { t.Fatalf("socks5 proxy hits = %d, want 1", got) } - if got := atomic.LoadInt32(&socks5hProxy.hits); got != 1 { + if got := socks5hProxy.hits.Load(); got != 1 { t.Fatalf("socks5h proxy hits = %d, want 1", got) } } func TestHTTPSRequestsRespectProxyModes(t *testing.T) { - var targetHits int32 + var targetHits atomic.Int32 target := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - atomic.AddInt32(&targetHits, 1) + targetHits.Add(1) w.Header().Set("Connection", "close") _, _ = io.WriteString(w, "https-target") })) @@ -363,7 +363,7 @@ func TestHTTPSRequestsRespectProxyModes(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - atomic.StoreInt32(&targetHits, 0) + targetHits.Store(0) atomic.StoreInt32(&proxyHits, 0) tr := mappedTransport(t, tt.spec, "service.test:443", targetAddr) @@ -381,7 +381,7 @@ func TestHTTPSRequestsRespectProxyModes(t *testing.T) { if string(body) != "https-target" { t.Fatalf("body = %q, want https-target", body) } - if got := atomic.LoadInt32(&targetHits); got != 1 { + if got := targetHits.Load(); got != 1 { t.Fatalf("target hits = %d, want 1", got) } if got := atomic.LoadInt32(&proxyHits); got != tt.wantProxyHits { @@ -433,7 +433,7 @@ func mappedTransport(t *testing.T, spec ProxySpec, fromAddr, toAddr string) *htt type socksHTTPProxy struct { addr string - hits int32 + hits atomic.Int32 } func newSocksHTTPProxy(t *testing.T) *socksHTTPProxy { @@ -480,7 +480,7 @@ func (p *socksHTTPProxy) handle(conn net.Conn) { if !p.readSocksAddr(r, req[3]) { return } - atomic.AddInt32(&p.hits, 1) + p.hits.Add(1) if _, err := conn.Write([]byte{5, 0, 0, 1, 0, 0, 0, 0, 0, 0}); err != nil { return } diff --git a/internal/outputstyle/outputstyle.go b/internal/outputstyle/outputstyle.go index dbeca04711..73fbbf9550 100644 --- a/internal/outputstyle/outputstyle.go +++ b/internal/outputstyle/outputstyle.go @@ -9,6 +9,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "sort" "strings" @@ -67,13 +68,13 @@ func Dirs() []string { var dirs []string if os.Getenv("REASONIX_HOME") == "" { if home, err := os.UserHomeDir(); err == nil { - for i := len(conventionDirs) - 1; i >= 0; i-- { - dirs = append(dirs, filepath.Join(home, conventionDirs[i], "output-styles")) + for _, v := range slices.Backward(conventionDirs) { + dirs = append(dirs, filepath.Join(home, v, "output-styles")) } } } - for i := len(conventionDirs) - 1; i >= 0; i-- { - dirs = append(dirs, filepath.Join(".", conventionDirs[i], "output-styles")) + for _, v := range slices.Backward(conventionDirs) { + dirs = append(dirs, filepath.Join(".", v, "output-styles")) } return dirs } diff --git a/internal/permission/bash_readonly.go b/internal/permission/bash_readonly.go index 4d9ebb80ad..69a9ece38d 100644 --- a/internal/permission/bash_readonly.go +++ b/internal/permission/bash_readonly.go @@ -2,6 +2,7 @@ package permission import ( "encoding/json" + "slices" "strings" "reasonix/internal/shellsafe" @@ -123,10 +124,8 @@ func hasArgWithPrefix(args []string, prefix string) bool { func hasAnyArg(args []string, unsafe ...string) bool { for _, arg := range args { - for _, candidate := range unsafe { - if arg == candidate { - return true - } + if slices.Contains(unsafe, arg) { + return true } } return false diff --git a/internal/plugin/cache_test.go b/internal/plugin/cache_test.go index 776e213b46..5820796dfd 100644 --- a/internal/plugin/cache_test.go +++ b/internal/plugin/cache_test.go @@ -266,7 +266,7 @@ func TestSchemaCacheKeyStable(t *testing.T) { // but a fresh map can incidentally iterate the same way, so we run a few // iterations to give the runtime a chance to shuffle). reordered := spec - for i := 0; i < 32; i++ { + for range 32 { reordered.Env = map[string]string{"BAR": "2", "FOO": "1"} if got := SchemaCacheKey(reordered); got != h1 { t.Fatalf("SchemaCacheKey changed when env was rebuilt: %q vs %q", got, h1) diff --git a/internal/plugin/chrome_devtools_live_test.go b/internal/plugin/chrome_devtools_live_test.go index 39d0ea5e69..6e2d49462b 100644 --- a/internal/plugin/chrome_devtools_live_test.go +++ b/internal/plugin/chrome_devtools_live_test.go @@ -35,8 +35,7 @@ func TestChromeDevtoolsMCPLive(t *testing.T) { stdioShellPATH = func(context.Context) string { return os.Getenv("PATH") } t.Cleanup(func() { stdioShellPATH = oldShellPATH }) - lifeCtx, lifeCancel := context.WithCancel(context.Background()) - defer lifeCancel() + lifeCtx := t.Context() callCtx, callCancel := context.WithTimeout(lifeCtx, 2*time.Minute) defer callCancel() diff --git a/internal/plugin/codegraph_limit_test.go b/internal/plugin/codegraph_limit_test.go index ad6fdbb325..30b3dec82a 100644 --- a/internal/plugin/codegraph_limit_test.go +++ b/internal/plugin/codegraph_limit_test.go @@ -9,7 +9,7 @@ func TestAcquireCodeGraphSlotCapsInstances(t *testing.T) { } releases := make([]func(), 0, maxCodeGraphInstances) - for i := 0; i < maxCodeGraphInstances; i++ { + for i := range maxCodeGraphInstances { release, err := acquireCodeGraphSlot() if err != nil { t.Fatalf("acquire %d within cap should succeed: %v", i, err) diff --git a/internal/plugin/host_concurrency_test.go b/internal/plugin/host_concurrency_test.go index e6e5a44223..141cc0d1a9 100644 --- a/internal/plugin/host_concurrency_test.go +++ b/internal/plugin/host_concurrency_test.go @@ -15,7 +15,7 @@ func TestHostConcurrentAccess(t *testing.T) { h := &Host{} // Seed a few "connected" servers so the read paths have data to walk. These // methods only read name/transport/toolCount, never the (nil) transport. - for i := 0; i < 4; i++ { + for i := range 4 { h.clients = append(h.clients, &Client{name: fmt.Sprintf("srv-%d", i), transport: "stdio", toolCount: i}) h.prompts = append(h.prompts, Prompt{Server: fmt.Sprintf("srv-%d", i), Name: "p"}) } @@ -23,10 +23,10 @@ func TestHostConcurrentAccess(t *testing.T) { const workers = 24 var wg sync.WaitGroup wg.Add(workers) - for w := 0; w < workers; w++ { + for w := range workers { go func(w int) { defer wg.Done() - for i := 0; i < 500; i++ { + for i := range 500 { switch (w + i) % 6 { case 0: h.RecordFailure(Spec{Name: fmt.Sprintf("bad-%d", i%8), Type: "stdio"}, errors.New("boom")) diff --git a/internal/plugin/hotadd_test.go b/internal/plugin/hotadd_test.go index a4d91c8cfa..491884f67d 100644 --- a/internal/plugin/hotadd_test.go +++ b/internal/plugin/hotadd_test.go @@ -84,7 +84,7 @@ func TestHostAddConcurrentSameServerReusesSingleClient(t *testing.T) { errs := make([]error, callers) counts := make([]int, callers) wg.Add(callers) - for i := 0; i < callers; i++ { + for i := range callers { go func(i int) { defer wg.Done() tools, err := h.Add(ctx, spec) diff --git a/internal/plugin/known_overrides.go b/internal/plugin/known_overrides.go index 7b5daa3b18..f020d7d6f0 100644 --- a/internal/plugin/known_overrides.go +++ b/internal/plugin/known_overrides.go @@ -1,7 +1,9 @@ package plugin import ( + "maps" "path/filepath" + "slices" "strings" ) @@ -50,12 +52,7 @@ func isCodebaseMemorySpec(s Spec) bool { if isCodebaseMemoryID(s.Name) || isCodebaseMemoryCommand(s.Command) { return true } - for _, arg := range s.Args { - if isCodebaseMemoryID(arg) { - return true - } - } - return false + return slices.ContainsFunc(s.Args, isCodebaseMemoryID) } func isCodebaseMemoryCommand(command string) bool { @@ -89,9 +86,7 @@ func isStdioSpecType(typ string) bool { func mergeDefaultEnv(existing map[string]string, key, value string) map[string]string { out := make(map[string]string, len(existing)+1) - for name, v := range existing { - out[name] = v - } + maps.Copy(out, existing) if _, ok := out[key]; !ok { out[key] = value } diff --git a/internal/plugin/launcher_lock.go b/internal/plugin/launcher_lock.go index 6b1a16d231..a32d39e919 100644 --- a/internal/plugin/launcher_lock.go +++ b/internal/plugin/launcher_lock.go @@ -71,8 +71,8 @@ func launcherLocatorForSpec(spec Spec) (launcherLocator, bool) { if arg == "--from" && i+1 < len(args) { return launcherLocator{kind: kind, value: args[i+1], arg: i + 1, command: command}, true } - if strings.HasPrefix(arg, "--from=") { - return launcherLocator{kind: kind, value: strings.TrimPrefix(arg, "--from="), arg: i, prefix: "--from=", command: command}, true + if after, ok := strings.CutPrefix(arg, "--from="); ok { + return launcherLocator{kind: kind, value: after, arg: i, prefix: "--from=", command: command}, true } } } diff --git a/internal/plugin/launcher_lock_test.go b/internal/plugin/launcher_lock_test.go index e9f7d927b3..43252d3815 100644 --- a/internal/plugin/launcher_lock_test.go +++ b/internal/plugin/launcher_lock_test.go @@ -8,6 +8,7 @@ import ( "path/filepath" "reflect" "runtime" + "slices" "strings" "testing" @@ -149,12 +150,7 @@ func TestStoredUVXFromLauncherLockKeepsFromValueAdjacent(t *testing.T) { } func stringSliceContains(values []string, want string) bool { - for _, value := range values { - if value == want { - return true - } - } - return false + return slices.Contains(values, want) } func TestMutableLauncherRejectsAmbiguousFlagValue(t *testing.T) { diff --git a/internal/plugin/lazy_test.go b/internal/plugin/lazy_test.go index 525ce8bdb3..0292028f72 100644 --- a/internal/plugin/lazy_test.go +++ b/internal/plugin/lazy_test.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "strings" "sync" "testing" @@ -93,10 +94,8 @@ func waitForServer(t *testing.T, host *Host, name string, timeout time.Duration) t.Helper() deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { - for _, n := range host.ServerNames() { - if n == name { - return - } + if slices.Contains(host.ServerNames(), name) { + return } time.Sleep(10 * time.Millisecond) } @@ -541,9 +540,7 @@ func TestLazySwapDoesNotRaceRegistrySchemas(t *testing.T) { done := make(chan struct{}) var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { for { select { case <-done: @@ -552,7 +549,7 @@ func TestLazySwapDoesNotRaceRegistrySchemas(t *testing.T) { _ = reg.Schemas() } } - }() + }) out, err := echo.Execute(ctx, json.RawMessage(`{"msg":"race"}`)) close(done) @@ -723,7 +720,7 @@ func TestLazyConcurrentExecuteOnlyOneSpawn(t *testing.T) { results := make([]string, goroutines) errs := make([]error, goroutines) wg.Add(goroutines) - for i := 0; i < goroutines; i++ { + for i := range goroutines { go func(i int) { defer wg.Done() out, err := echo.Execute(ctx, json.RawMessage(fmt.Sprintf(`{"msg":"r%d"}`, i))) @@ -1052,8 +1049,7 @@ func TestAddWithLifecycleSurvivesHandshakeCtxCancel(t *testing.T) { host := NewHost() defer host.Close() - lifeCtx, cancelLife := context.WithCancel(context.Background()) - defer cancelLife() + lifeCtx := t.Context() handshakeCtx, cancelHandshake := context.WithTimeout(context.Background(), 5*time.Second) tools, err := host.AddWithLifecycle(lifeCtx, handshakeCtx, spec) cancelHandshake() // the proxy's deferred cancel fires right after connect diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go index 5eb1adc248..45c46eddca 100644 --- a/internal/plugin/plugin.go +++ b/internal/plugin/plugin.go @@ -15,6 +15,7 @@ import ( "hash/fnv" "io" "log/slog" + "maps" "reflect" "regexp" "sort" @@ -390,8 +391,7 @@ func Start(ctx context.Context, specs []Spec, p StartPolicy) (*Host, []tool.Tool phaseADur := recordedPhaseADur() cancelStartup() if !p.SkipPersistence { - h.bgWrites.Add(1) - go func() { defer h.bgWrites.Done(); _ = RecordStartup(spec.Name, phaseADur) }() + h.bgWrites.Go(func() { ; _ = RecordStartup(spec.Name, phaseADur) }) } ch <- result{idx: idx, spec: spec, err: fmt.Errorf("start plugin %q: %w", spec.Name, err)} return @@ -402,8 +402,7 @@ func Start(ctx context.Context, specs []Spec, p StartPolicy) (*Host, []tool.Tool phaseADur := recordedPhaseADur() cancelStartup() if !p.SkipPersistence { - h.bgWrites.Add(1) - go func() { defer h.bgWrites.Done(); _ = RecordStartup(spec.Name, phaseADur) }() + h.bgWrites.Go(func() { ; _ = RecordStartup(spec.Name, phaseADur) }) } c.close() err = newStartupFailure("tools/list", phaseAStart, c.startupStderr(), err) @@ -418,9 +417,7 @@ func Start(ctx context.Context, specs []Spec, p StartPolicy) (*Host, []tool.Tool phaseADur := recordedPhaseADur() cancelStartup() if !p.SkipPersistence { - h.bgWrites.Add(1) - go func() { - defer h.bgWrites.Done() + h.bgWrites.Go(func() { _ = RecordStartup(spec.Name, phaseADur) _ = SaveCachedSchema(spec.Name, CachedSchema{ CacheKey: SchemaCacheKey(spec), @@ -431,7 +428,7 @@ func Start(ctx context.Context, specs []Spec, p StartPolicy) (*Host, []tool.Tool }, Tools: cacheableToolsOf(ts), }) - }() + }) } // Prompts and resources are deferred to StartPhaseB so the boot path @@ -506,11 +503,9 @@ func (h *Host) Close() { // Callers must enqueue before their Close-drained startup owner completes, so // Close cannot begin waiting before the WaitGroup increment is visible. func (h *Host) queueBackgroundWrite(write func()) { - h.bgWrites.Add(1) - go func() { - defer h.bgWrites.Done() + h.bgWrites.Go(func() { write() - }() + }) } // StartPhaseB asynchronously fetches the auxiliary surfaces (prompts and @@ -1544,14 +1539,10 @@ func (c *Client) withProgress(ctx context.Context, method string, params any) (a token := fmt.Sprintf("reasonix-%d", c.progressID.Add(1)) copyParams := make(map[string]any, len(callParams)) - for key, value := range callParams { - copyParams[key] = value - } + maps.Copy(copyParams, callParams) meta := map[string]any{} if existing, ok := callParams["_meta"].(map[string]any); ok { - for key, value := range existing { - meta[key] = value - } + maps.Copy(meta, existing) } meta["progressToken"] = token copyParams["_meta"] = meta diff --git a/internal/plugin/plugin_test.go b/internal/plugin/plugin_test.go index 84687bcd65..b0084a57eb 100644 --- a/internal/plugin/plugin_test.go +++ b/internal/plugin/plugin_test.go @@ -779,8 +779,7 @@ func TestStdioFailureCapturesStderr(t *testing.T) { } func TestStartupFailureReportsStageElapsedAndRedactedStderr(t *testing.T) { - lifeCtx, cancelLife := context.WithCancel(context.Background()) - defer cancelLife() + lifeCtx := t.Context() startupCtx, cancelStartup := context.WithTimeout(lifeCtx, 40*time.Millisecond) defer cancelStartup() @@ -828,8 +827,7 @@ func TestFailureSummaryRedactsCredentials(t *testing.T) { } func TestEnsureConnectedInBackgroundSurvivesShortCallerWait(t *testing.T) { - lifeCtx, cancelLife := context.WithCancel(context.Background()) - defer cancelLife() + lifeCtx := t.Context() host := NewHost() defer host.Close() spec := Spec{ @@ -863,8 +861,7 @@ func TestEnsureConnectedInBackgroundSurvivesShortCallerWait(t *testing.T) { } func TestEnsureConnectedInBackgroundRemoveDoesNotResurrectServer(t *testing.T) { - lifeCtx, cancelLife := context.WithCancel(context.Background()) - defer cancelLife() + lifeCtx := t.Context() host := NewHost() defer host.Close() spec := Spec{ @@ -1129,7 +1126,7 @@ func TestStartRecordsTimeoutStats(t *testing.T) { "GO_WANT_HELPER_INIT_MS": "300", }, } - for i := 0; i < 3; i++ { + for i := range 3 { host, _, err := Start(ctx, []Spec{slow}, StartPolicy{ PerPluginTimeout: 50 * time.Millisecond, Concurrency: 1, diff --git a/internal/plugin/process_mode_test.go b/internal/plugin/process_mode_test.go index 3ca09cf753..d9e32cbb2e 100644 --- a/internal/plugin/process_mode_test.go +++ b/internal/plugin/process_mode_test.go @@ -52,13 +52,13 @@ func TestHostModeEnsureConnectedSkipsCommandSandboxAndSharesProcess(t *testing.T err error } ch := make(chan result, 3) - for i := 0; i < 3; i++ { + for range 3 { go func() { tools, err := host.EnsureConnected(ctx, spec) ch <- result{len(tools), err} }() } - for i := 0; i < 3; i++ { + for range 3 { r := <-ch if r.err != nil { t.Fatalf("EnsureConnected: %v", r.err) diff --git a/internal/plugin/protocol_inbound.go b/internal/plugin/protocol_inbound.go index 8d8d0bb45c..02445b75ba 100644 --- a/internal/plugin/protocol_inbound.go +++ b/internal/plugin/protocol_inbound.go @@ -114,8 +114,8 @@ func mcpRoots(workspaceRoot string) []mcpRoot { clean := filepath.Clean(abs) path := filepath.ToSlash(clean) fileURL := &url.URL{Scheme: "file"} - if strings.HasPrefix(path, "//") { - parts := strings.SplitN(strings.TrimPrefix(path, "//"), "/", 2) + if after, ok := strings.CutPrefix(path, "//"); ok { + parts := strings.SplitN(after, "/", 2) fileURL.Host = parts[0] if len(parts) == 2 { fileURL.Path = "/" + parts[1] diff --git a/internal/plugin/proxy_e2e_test.go b/internal/plugin/proxy_e2e_test.go index 5bb83b10bc..0e0ed8e574 100644 --- a/internal/plugin/proxy_e2e_test.go +++ b/internal/plugin/proxy_e2e_test.go @@ -33,8 +33,7 @@ func mockSpec() plugin.Spec { func TestUseCapabilityFirstDiscoveryConnectListCall(t *testing.T) { host := plugin.NewHost() defer host.Close() - lifeCtx, cancel := context.WithCancel(context.Background()) - defer cancel() + lifeCtx := t.Context() ledger := capability.NewLedger() audit := &capability.Audit{} proxy := agent.NewUseCapabilityTool(lifeCtx, host, []plugin.Spec{mockSpec()}, tool.NewRegistry(), ledger, audit, nil) @@ -110,8 +109,7 @@ func TestUseCapabilityFirstDiscoveryConnectListCall(t *testing.T) { func TestUseCapabilitySharedHostSnapshot(t *testing.T) { host := plugin.NewHost() defer host.Close() - lifeCtx, cancel := context.WithCancel(context.Background()) - defer cancel() + lifeCtx := t.Context() callCtx, cancelCall := context.WithTimeout(context.Background(), 5*time.Second) defer cancelCall() // "Tab A" connects directly (not through any proxy). diff --git a/internal/plugin/startup.go b/internal/plugin/startup.go index 35c37dbaed..cd0297bf74 100644 --- a/internal/plugin/startup.go +++ b/internal/plugin/startup.go @@ -73,10 +73,7 @@ func newStartupFailure(stage string, started time.Time, stderr string, err error if errors.As(err, &existing) { return err } - elapsed := time.Since(started) - if elapsed < 0 { - elapsed = 0 - } + elapsed := max(time.Since(started), 0) return &startupFailure{ Stage: strings.TrimSpace(stage), Elapsed: elapsed, diff --git a/internal/plugin/stats.go b/internal/plugin/stats.go index 7879c0a0d8..a0e17de2f3 100644 --- a/internal/plugin/stats.go +++ b/internal/plugin/stats.go @@ -16,7 +16,7 @@ import ( "log/slog" "os" "path/filepath" - "sort" + "slices" "time" "reasonix/internal/config" @@ -93,10 +93,7 @@ func RecordStartup(name string, dur time.Duration) error { } stats.Name = name - ms := dur.Milliseconds() - if ms < 0 { - ms = 0 - } + ms := max(dur.Milliseconds(), 0) stats.SamplesMs = append(stats.SamplesMs, ms) if len(stats.SamplesMs) > maxSamples { // Trim from the front: oldest samples leave first. @@ -278,13 +275,10 @@ func p99(samples []int64) time.Duration { return 0 } sorted := append([]int64(nil), samples...) - sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] }) + slices.Sort(sorted) // Use ceil(0.99 * n) - 1 so that for n=1..100 we always pick the last // element; for larger n it's the index at the 99% boundary. - idx := int(float64(len(sorted))*0.99+0.9999999) - 1 - if idx < 0 { - idx = 0 - } + idx := max(int(float64(len(sorted))*0.99+0.9999999)-1, 0) if idx >= len(sorted) { idx = len(sorted) - 1 } diff --git a/internal/plugin/stats_test.go b/internal/plugin/stats_test.go index f4093d5b68..87ed162520 100644 --- a/internal/plugin/stats_test.go +++ b/internal/plugin/stats_test.go @@ -73,7 +73,7 @@ func TestRecordStartupCapsAtWindow(t *testing.T) { withTempCache(t) const total = 25 - for i := 0; i < total; i++ { + for i := range total { // Use distinct values so we can verify the *oldest* ones were dropped. if err := RecordStartup("bar", time.Duration(i+1)*time.Millisecond); err != nil { t.Fatalf("RecordStartup #%d: %v", i, err) @@ -140,7 +140,7 @@ func TestRecommendAtBudgetDemotes(t *testing.T) { withTempCache(t) budget := 5 * time.Second - for i := 0; i < 3; i++ { + for i := range 3 { if err := RecordStartup("timeout-plugin", budget); err != nil { t.Fatalf("RecordStartup #%d: %v", i, err) } @@ -231,7 +231,7 @@ func TestStatsSlugCollisionDoesNotCrossDemote(t *testing.T) { withTempCache(t) // Interleave a chronically slow server with a healthy one sharing the slug. - for i := 0; i < defaultDemoteAfter; i++ { + for range defaultDemoteAfter { if err := RecordStartup("foo.bar", 5*time.Second); err != nil { t.Fatalf("RecordStartup foo.bar: %v", err) } @@ -264,7 +264,7 @@ func TestStatsLegacyNamedFileMigrated(t *testing.T) { withTempCache(t) legacy := StartupStats{Version: statsVersion, Name: "legacy", LastSeen: time.Now()} - for i := 0; i < 3; i++ { + for range 3 { legacy.SamplesMs = append(legacy.SamplesMs, 100) } if err := writeStatsAtomic(legacyStatsPath("legacy"), legacy); err != nil { @@ -291,7 +291,7 @@ func TestStatsLegacyNamelessCollisionNotTrusted(t *testing.T) { withTempCache(t) nameless := StartupStats{Version: statsVersion, LastSeen: time.Now()} - for i := 0; i < defaultDemoteAfter; i++ { + for range defaultDemoteAfter { nameless.SamplesMs = append(nameless.SamplesMs, 5000) // over budget } if err := writeStatsAtomic(legacyStatsPath("foo.bar"), nameless); err != nil { @@ -322,7 +322,7 @@ func TestStatsLegacyNamelessCollisionNotTrusted(t *testing.T) { func TestStatsLegacyFileWithoutNameAdopted(t *testing.T) { withTempCache(t) - for i := 0; i < 3; i++ { + for range 3 { if err := RecordStartup("legacy", 100*time.Millisecond); err != nil { t.Fatalf("RecordStartup: %v", err) } diff --git a/internal/plugin/stdio_cancel_test.go b/internal/plugin/stdio_cancel_test.go index 1150d4b04c..4e10a82c7d 100644 --- a/internal/plugin/stdio_cancel_test.go +++ b/internal/plugin/stdio_cancel_test.go @@ -343,7 +343,7 @@ func TestStdioReadLoopStaysLiveWhenReplyWriterIsBlocked(t *testing.T) { // run off the test goroutine so a deadlocked readLoop fails the timeout // below instead of hanging the whole package; Cleanup unblocks the writer. go func() { - for i := 0; i < 2*stdioReplyQueueBound; i++ { + for i := range 2 * stdioReplyQueueBound { line := fmt.Sprintf(`{"jsonrpc":"2.0","id":"srv-%d","method":"ping"}`+"\n", i) if _, err := io.WriteString(stdoutWrites, line); err != nil { return diff --git a/internal/plugin/toolresult_image_test.go b/internal/plugin/toolresult_image_test.go index 8806936050..f2637bf95e 100644 --- a/internal/plugin/toolresult_image_test.go +++ b/internal/plugin/toolresult_image_test.go @@ -103,7 +103,7 @@ func TestParseToolResultImageOversizedOmitted(t *testing.T) { func TestParseToolResultImageCountCapped(t *testing.T) { payload := base64.StdEncoding.EncodeToString([]byte("x")) items := make([]string, 0, maxToolResultImages+1) - for i := 0; i < maxToolResultImages+1; i++ { + for range maxToolResultImages + 1 { items = append(items, imageItem("image/png", payload)) } res := `{"content":[` + strings.Join(items, ",") + `]}` diff --git a/internal/plugin/transport_http.go b/internal/plugin/transport_http.go index 0e31c08484..4fbcdc3c64 100644 --- a/internal/plugin/transport_http.go +++ b/internal/plugin/transport_http.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "io" + "maps" "net/http" "net/url" "strings" @@ -47,9 +48,7 @@ func newHTTPTransport(s Spec) (*httpTransport, error) { return nil, fmt.Errorf("http plugin %q: url is required", s.Name) } headers := make(map[string]string, len(s.Headers)) - for key, value := range s.Headers { - headers[key] = value - } + maps.Copy(headers, s.Headers) return &httpTransport{ name: s.Name, url: s.URL, diff --git a/internal/plugin/transport_sse.go b/internal/plugin/transport_sse.go index 4a330541c5..b1cd9abffd 100644 --- a/internal/plugin/transport_sse.go +++ b/internal/plugin/transport_sse.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "maps" "net/http" "net/url" "strings" @@ -60,9 +61,7 @@ func newSSETransport(ctx context.Context, s Spec) (*sseTransport, error) { return nil, fmt.Errorf("sse plugin %q: invalid url %q", s.Name, s.URL) } headers := make(map[string]string, len(s.Headers)) - for key, value := range s.Headers { - headers[key] = value - } + maps.Copy(headers, s.Headers) lifeCtx, cancel := context.WithCancel(ctx) t := &sseTransport{ name: s.Name, diff --git a/internal/plugin/transport_sse_test.go b/internal/plugin/transport_sse_test.go index 710f0f8fdc..5368a81714 100644 --- a/internal/plugin/transport_sse_test.go +++ b/internal/plugin/transport_sse_test.go @@ -258,7 +258,7 @@ func TestLegacySSEBoundsConcurrentServerRequestReplies(t *testing.T) { transport.mu.Lock() transport.pending[7] = waiting transport.mu.Unlock() - for i := 0; i < 2*sseReplyQueueBound; i++ { + for i := range 2 * sseReplyQueueBound { events <- fmt.Sprintf("event: message\ndata: {\"jsonrpc\":\"2.0\",\"id\":\"srv-%d\",\"method\":\"ping\"}\n\n", i) } events <- "event: message\ndata: {\"jsonrpc\":\"2.0\",\"id\":7,\"result\":{}}\n\n" diff --git a/internal/plugin/transport_stdio.go b/internal/plugin/transport_stdio.go index 6be11c220c..48df31a85a 100644 --- a/internal/plugin/transport_stdio.go +++ b/internal/plugin/transport_stdio.go @@ -11,6 +11,7 @@ import ( "os/exec" "path/filepath" "runtime" + "slices" "strings" "sync" "time" @@ -339,7 +340,7 @@ func executableNames(command, pathext string) []string { } names := []string{command} seen := map[string]bool{strings.ToLower(command): true} - for _, ext := range strings.Split(pathext, ";") { + for ext := range strings.SplitSeq(pathext, ";") { ext = strings.TrimSpace(ext) if ext == "" { continue @@ -475,9 +476,9 @@ func prepareStdioShellPATHProbe(cmd *exec.Cmd) { func parseShellPATH(out []byte, marker string) string { lines := strings.Split(strings.ReplaceAll(string(out), "\r\n", "\n"), "\n") - for i := len(lines) - 1; i >= 0; i-- { - if strings.HasPrefix(lines[i], marker) { - return strings.TrimSpace(strings.TrimPrefix(lines[i], marker)) + for _, line := range slices.Backward(lines) { + if rest, ok := strings.CutPrefix(line, marker); ok { + return strings.TrimSpace(rest) } } return "" @@ -512,8 +513,8 @@ func setEnvValue(env []string, key, value string) []string { } func envValue(env []string, key string) (string, bool) { - for i := len(env) - 1; i >= 0; i-- { - k, v, ok := strings.Cut(env[i], "=") + for _, entry := range slices.Backward(env) { + k, v, ok := strings.Cut(entry, "=") if ok && envKeyEqual(k, key) { return v, true } diff --git a/internal/pluginpkg/claude_compat.go b/internal/pluginpkg/claude_compat.go index e4cc51bd67..56e2ca8dd9 100644 --- a/internal/pluginpkg/claude_compat.go +++ b/internal/pluginpkg/claude_compat.go @@ -93,7 +93,7 @@ func claudeMatcherNeverFires(matcher string) bool { if matcher == "" || matcher == "*" { return false } - for _, part := range strings.Split(matcher, "|") { + for part := range strings.SplitSeq(matcher, "|") { part = strings.TrimSpace(part) if !bareClaudeToolNamePattern.MatchString(part) || !claudeUnsupportedToolMatchers[part] { return false @@ -524,7 +524,7 @@ func splitCSV(raw string) []string { raw = strings.TrimSpace(raw[1 : len(raw)-1]) } var out []string - for _, item := range strings.Split(raw, ",") { + for item := range strings.SplitSeq(raw, ",") { if item = strings.Trim(strings.TrimSpace(item), `"'`); item != "" { out = append(out, item) } diff --git a/internal/pluginpkg/manifest_v1.go b/internal/pluginpkg/manifest_v1.go index 7a8e2e05a0..fe1543e758 100644 --- a/internal/pluginpkg/manifest_v1.go +++ b/internal/pluginpkg/manifest_v1.go @@ -5,11 +5,13 @@ import ( "encoding/json" "errors" "fmt" + "maps" "os" "path" "path/filepath" "reflect" "regexp" + "slices" "sort" "strconv" "strings" @@ -426,9 +428,7 @@ func mergeV1MCPServers(legacy, contrib map[string]MCPServer) (map[string]MCPServ return legacy, nil } out := make(map[string]MCPServer, len(legacy)) - for name, server := range legacy { - out[name] = server - } + maps.Copy(out, legacy) for _, name := range sortedKeys(contrib) { server := contrib[name] if existing, ok := out[name]; ok { @@ -573,13 +573,7 @@ func parseV1Runtime(raw json.RawMessage) (*RuntimeSpec, error) { } } for _, capability := range rt.Capabilities { - known := false - for _, k := range runtimeCapabilities { - if capability == k { - known = true - break - } - } + known := slices.Contains(runtimeCapabilities, capability) if !known { return nil, fmt.Errorf("runtime.capabilities: unknown capability %q (want one of: %s)", capability, strings.Join(runtimeCapabilities, ", ")) } diff --git a/internal/pluginpkg/state_lock_test.go b/internal/pluginpkg/state_lock_test.go index 9334e355f9..022169f2b2 100644 --- a/internal/pluginpkg/state_lock_test.go +++ b/internal/pluginpkg/state_lock_test.go @@ -19,9 +19,7 @@ func TestStateConcurrentUpsertAndSetEnabled(t *testing.T) { var wg sync.WaitGroup for i := range n { name := fmt.Sprintf("plugin-%02d", i) - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { if err := Upsert(home, InstalledPlugin{Name: name, Root: "plugins/" + name}); err != nil { t.Errorf("Upsert(%s): %v", name, err) return @@ -29,7 +27,7 @@ func TestStateConcurrentUpsertAndSetEnabled(t *testing.T) { if err := SetEnabled(home, name, true); err != nil { t.Errorf("SetEnabled(%s): %v", name, err) } - }() + }) } wg.Wait() @@ -62,9 +60,7 @@ func TestStateConcurrentRemove(t *testing.T) { var wg sync.WaitGroup for i := range n { name := fmt.Sprintf("plugin-%02d", i) - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { removed, ok, err := Remove(home, name) if err != nil { t.Errorf("Remove(%s): %v", name, err) @@ -73,7 +69,7 @@ func TestStateConcurrentRemove(t *testing.T) { if !ok || removed.Name != name { t.Errorf("Remove(%s) = %+v, ok=%v", name, removed, ok) } - }() + }) } wg.Wait() diff --git a/internal/productdocs/docs_test.go b/internal/productdocs/docs_test.go index 48824c68c0..fde2403c76 100644 --- a/internal/productdocs/docs_test.go +++ b/internal/productdocs/docs_test.go @@ -450,7 +450,7 @@ func TestDocsToolSupportsConcurrentReadOnlyCalls(t *testing.T) { const workers = 12 var wg sync.WaitGroup errs := make(chan error, workers) - for i := 0; i < workers; i++ { + for i := range workers { wg.Add(1) go func(index int) { defer wg.Done() diff --git a/internal/provider/anthropic/anthropic_test.go b/internal/provider/anthropic/anthropic_test.go index 839f8438cd..5f05729185 100644 --- a/internal/provider/anthropic/anthropic_test.go +++ b/internal/provider/anthropic/anthropic_test.go @@ -106,8 +106,7 @@ func TestBuildRequestKeepsDefaultCacheControlBytesStable(t *testing.T) { if err != nil { t.Fatalf("marshal first request: %v", err) } - requestCtx, cancel := context.WithCancel(context.Background()) - defer cancel() + requestCtx := t.Context() got, err := json.Marshal(c.buildRequest(requestCtx, req)) if err != nil { t.Fatalf("marshal second request: %v", err) diff --git a/internal/provider/openai/host.go b/internal/provider/openai/host.go index 90b23924f9..ec7eb1e6eb 100644 --- a/internal/provider/openai/host.go +++ b/internal/provider/openai/host.go @@ -2,6 +2,7 @@ package openai import ( "net/url" + "slices" "strings" "reasonix/internal/provider" @@ -24,10 +25,8 @@ func matchesVendorHost(baseURL, apex string, canonical ...string) bool { return false } host := strings.ToLower(u.Hostname()) - for _, c := range canonical { - if host == c { - return true - } + if slices.Contains(canonical, host) { + return true } return strings.HasSuffix(host, "."+apex) } diff --git a/internal/provider/openai/openai.go b/internal/provider/openai/openai.go index 14bbaaca75..4f0bbd8f7a 100644 --- a/internal/provider/openai/openai.go +++ b/internal/provider/openai/openai.go @@ -29,6 +29,7 @@ import ( "errors" "fmt" "io" + "maps" "net/http" "sort" "strings" @@ -1225,9 +1226,7 @@ func (r chatRequest) MarshalJSON() ([]byte, error) { if err := json.Unmarshal(raw, &body); err != nil { return nil, err } - for key, value := range cleanExtraBody(r.ExtraBody) { - body[key] = value - } + maps.Copy(body, cleanExtraBody(r.ExtraBody)) return json.Marshal(body) } diff --git a/internal/provider/openai/openai_test.go b/internal/provider/openai/openai_test.go index 6381048afa..4e1c7f78b5 100644 --- a/internal/provider/openai/openai_test.go +++ b/internal/provider/openai/openai_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "io" + "maps" "net/http" "net/http/httptest" "strings" @@ -1677,9 +1678,7 @@ func withEffort(c provider.Config, effort string) provider.Config { extra = map[string]any{} } else { cp := make(map[string]any, len(extra)+1) - for k, v := range extra { - cp[k] = v - } + maps.Copy(cp, extra) extra = cp } extra["effort"] = effort diff --git a/internal/provider/openai/think.go b/internal/provider/openai/think.go index 8873052572..1592c46f21 100644 --- a/internal/provider/openai/think.go +++ b/internal/provider/openai/think.go @@ -88,10 +88,7 @@ func (t *thinkSplitter) drainPassthrough() string { // prefix of marker — the tail to hold back in case the rest of the tag arrives // in the next delta. func markerSuffixLen(s, marker string) int { - max := len(marker) - 1 - if max > len(s) { - max = len(s) - } + max := min(len(marker)-1, len(s)) for n := max; n > 0; n-- { if strings.HasPrefix(marker, s[len(s)-n:]) { return n diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 99cc55e548..4da6fb204f 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -13,6 +13,7 @@ import ( "fmt" "io" "net" + "slices" "sort" "strings" "syscall" @@ -515,7 +516,7 @@ func repairToolCallArgs(m Message) Message { func closeTruncatedJSON(s string) string { var stack []byte inStr, esc := false, false - for i := 0; i < len(s); i++ { + for i := range len(s) { c := s[i] if inStr { switch { @@ -555,8 +556,8 @@ func closeTruncatedJSON(s string) string { case strings.HasSuffix(trimmed, ":"): out = trimmed + "null" } - for i := len(stack) - 1; i >= 0; i-- { - out += string(stack[i]) + for _, v := range slices.Backward(stack) { + out += string(v) } if !json.Valid([]byte(out)) { return "{}" @@ -789,13 +790,7 @@ func (p *Pricing) Cost(u *Usage) float64 { // writes or 2x 1-hour writes). Older providers leave both fields at zero and // keep the legacy one-input-rate behavior. A write count without billed // units also falls back to 1x for backward compatibility. - write := u.CacheWriteTokens - if write < 0 { - write = 0 - } - if write > miss { - write = miss - } + write := min(max(u.CacheWriteTokens, 0), miss) billedWrite := 0.0 if write > 0 { billedWrite = u.CacheWriteBilledTokens diff --git a/internal/provider/responses/responses.go b/internal/provider/responses/responses.go index d110c280e9..6144470506 100644 --- a/internal/provider/responses/responses.go +++ b/internal/provider/responses/responses.go @@ -796,10 +796,7 @@ func usageFromResponse(response *sseResponse) *provider.Usage { if u.OutputTokensDetails != nil { reasoning = u.OutputTokensDetails.ReasoningTokens } - miss := u.InputTokens - cached - if miss < 0 { - miss = 0 - } + miss := max(u.InputTokens-cached, 0) total := u.TotalTokens if total == 0 { total = u.InputTokens + u.OutputTokens diff --git a/internal/provider/responses/responses_test.go b/internal/provider/responses/responses_test.go index ac205cbe3d..47f09e1eaf 100644 --- a/internal/provider/responses/responses_test.go +++ b/internal/provider/responses/responses_test.go @@ -251,18 +251,18 @@ func TestStreamDoesNotDuplicateDoneText(t *testing.T) { defer server.Close() chunks := collect(t, New(Config{Name: "test", APIKey: "key", BaseURL: server.URL, Model: "m", Mode: "stateless"}), provider.Request{Messages: []provider.Message{{Role: provider.RoleUser, Content: "hi"}}}) - var text string + var text strings.Builder var usage *provider.Usage for _, chunk := range chunks { if chunk.Type == provider.ChunkText { - text += chunk.Text + text.WriteString(chunk.Text) } if chunk.Type == provider.ChunkUsage { usage = chunk.Usage } } - if text != "hello" { - t.Fatalf("streamed text = %q, want one copy", text) + if text.String() != "hello" { + t.Fatalf("streamed text = %q, want one copy", text.String()) } if usage == nil || usage.CacheHitTokens != 2 || usage.CacheMissTokens != 1 || usage.ReasoningTokens != 1 || usage.RequestCount != 1 { t.Fatalf("usage = %+v", usage) @@ -287,16 +287,16 @@ func TestStreamToleratesWebSearchLifecycleEvents(t *testing.T) { chunks := collect(t, New(Config{Name: "deepseek", APIKey: "key", BaseURL: server.URL, Model: "deepseek-v4-flash", Mode: "stateless", WebSearch: true}), provider.Request{ Messages: []provider.Message{{Role: provider.RoleUser, Content: "search"}}, }) - var text string + var text strings.Builder for _, chunk := range chunks { if chunk.Type == provider.ChunkText { - text += chunk.Text + text.WriteString(chunk.Text) } if chunk.Type == provider.ChunkError { t.Fatalf("unexpected stream error: %v", chunk.Err) } } - if text != "found it" || chunks[len(chunks)-1].Type != provider.ChunkDone { + if text.String() != "found it" || chunks[len(chunks)-1].Type != provider.ChunkDone { t.Fatalf("chunks = %#v, want searched answer followed by done", chunks) } } diff --git a/internal/provider/retry.go b/internal/provider/retry.go index 627c3edc70..a7e9e82e6d 100644 --- a/internal/provider/retry.go +++ b/internal/provider/retry.go @@ -225,10 +225,7 @@ func backoffDelay(attempt int, retryAfter time.Duration) time.Duration { } return retryAfter } - d := time.Duration(1<<(attempt-1)) * 500 * time.Millisecond - if d > maxBackoff { - d = maxBackoff - } + d := min(time.Duration(1<<(attempt-1))*500*time.Millisecond, maxBackoff) return d + time.Duration(rand.Intn(250))*time.Millisecond } diff --git a/internal/provider/retry_test.go b/internal/provider/retry_test.go index 74857d683e..7c9023ef8f 100644 --- a/internal/provider/retry_test.go +++ b/internal/provider/retry_test.go @@ -172,7 +172,7 @@ func TestSendWithRetryRetriesTransientAuthForKnownKey(t *testing.T) { if err != nil { t.Fatalf("a previously-good key should recover from a transient 401: %v", err) } - if resp.StatusCode != 200 || calls != 3 { + if resp.StatusCode != http.StatusOK || calls != 3 { t.Fatalf("status=%d calls=%d, want 200 after 3 calls", resp.StatusCode, calls) } } @@ -228,7 +228,7 @@ func TestSendWithRetryUnblocksStalledErrorBody(t *testing.T) { cl := &http.Client{Transport: rtFunc(func(r *http.Request) (*http.Response, error) { calls++ if calls == 1 { - return &http.Response{StatusCode: 502, Body: newStallingBody(), Header: http.Header{}}, nil + return &http.Response{StatusCode: http.StatusBadGateway, Body: newStallingBody(), Header: http.Header{}}, nil } return statusResp(200, nil), nil })} @@ -248,7 +248,7 @@ func TestSendWithRetryUnblocksStalledErrorBody(t *testing.T) { if r.err != nil { t.Fatalf("should recover after the stalled 502: %v", r.err) } - if r.resp.StatusCode != 200 || calls != 2 { + if r.resp.StatusCode != http.StatusOK || calls != 2 { t.Fatalf("status=%d calls=%d, want 200 after 2 calls", r.resp.StatusCode, calls) } case <-time.After(5 * time.Second): @@ -273,7 +273,7 @@ func TestSendWithRetryRecoversAndNotifies(t *testing.T) { if err != nil { t.Fatalf("should recover after one retry: %v", err) } - if resp.StatusCode != 200 || calls != 2 { + if resp.StatusCode != http.StatusOK || calls != 2 { t.Fatalf("status=%d calls=%d, want 200 after 2 calls", resp.StatusCode, calls) } if len(infos) != 1 || infos[0].Attempt != 1 || infos[0].Max != MaxRetries { diff --git a/internal/recovery/decision_test.go b/internal/recovery/decision_test.go index 419500477b..fdcc66f76e 100644 --- a/internal/recovery/decision_test.go +++ b/internal/recovery/decision_test.go @@ -314,7 +314,7 @@ func TestBehaviorMatrixGolden(t *testing.T) { }) t.Run("third repeated operation stops without prompting", func(t *testing.T) { got := run(t, func(g *Gate) { - for i := 0; i < 3; i++ { + for range 3 { g.ObserveResult(context.Background(), Observation{ Tool: "write_file", Mutates: true, Subject: "a.go", ErrSummary: "fail", Args: json.RawMessage(`{"path":"a.go","content":"x"}`), @@ -330,7 +330,7 @@ func TestBehaviorMatrixGolden(t *testing.T) { }) t.Run("different edit after three verification failures stays automatic", func(t *testing.T) { got := run(t, func(g *Gate) { - for i := 0; i < 3; i++ { + for range 3 { g.ObserveResult(context.Background(), Observation{ Tool: "bash", Verification: true, Subject: "go test ./...", ErrSummary: "fail", Args: json.RawMessage(`{"command":"go test ./..."}`), diff --git a/internal/recovery/gate_test.go b/internal/recovery/gate_test.go index 5ee91161af..526e2a17ec 100644 --- a/internal/recovery/gate_test.go +++ b/internal/recovery/gate_test.go @@ -468,7 +468,7 @@ func TestRepeatedFailureStopsOnlyTheSameOperation(t *testing.T) { Preview: "mvn test", Verification: true, Args: failedArgs, } g.ObserveResult(context.Background(), failed) - for attempt := 0; attempt < 2; attempt++ { + for attempt := range 2 { dec, err := g.BeforeMutation(context.Background(), retry) if err != nil || !dec.Allow { t.Fatalf("retry %d = %+v, %v", attempt+1, dec, err) @@ -492,7 +492,7 @@ func TestRepeatedFailureStopsOnlyTheSameOperation(t *testing.T) { func TestDifferentFailureStartsFreshRecoveryEpisode(t *testing.T) { g := NewGate(Options{}) - for i := 0; i < 2; i++ { + for range 2 { g.ObserveResult(context.Background(), Observation{ TaskScopeID: "turn:1", Tool: "bash", Subject: "go test ./...", Verification: true, Args: json.RawMessage(`{"command":"go test ./..."}`), ErrSummary: "go failed", @@ -511,7 +511,7 @@ func TestDifferentFailureStartsFreshRecoveryEpisode(t *testing.T) { func TestNewOrdinaryTurnRetiresTechnicalFailureLatch(t *testing.T) { g := NewGate(Options{}) args := json.RawMessage(`{"command":"go test ./..."}`) - for i := 0; i < 3; i++ { + for range 3 { g.ObserveResult(context.Background(), Observation{ TaskScopeID: "turn:1", Tool: "bash", Subject: "go test ./...", Verification: true, Args: args, ErrSummary: "fail", @@ -535,7 +535,7 @@ func TestLeavingAutoRetiresTechnicalFailureLatch(t *testing.T) { mode := "auto" g := NewGate(Options{Mode: func() string { return mode }}) args := json.RawMessage(`{"command":"go test ./..."}`) - for i := 0; i < 3; i++ { + for range 3 { g.ObserveResult(context.Background(), Observation{ TaskScopeID: "goal:ship", Tool: "bash", Subject: "go test ./...", Verification: true, Args: args, ErrSummary: "fail", @@ -608,7 +608,7 @@ func TestReviewerRejectBudgetIsEpisodeCumulative(t *testing.T) { Args: argsA, ErrSummary: "fail", }) proposalA := Proposal{TaskScopeID: "turn:1", Tool: "write_file", Subject: "a.go", Mutates: true, Args: argsA} - for i := 0; i < 3; i++ { + for i := range 3 { dec, err := g.BeforeMutation(context.Background(), proposalA) if err != nil || dec.Allow || !dec.Blocked { t.Fatalf("proposal A attempt %d = %+v, %v", i+1, dec, err) @@ -738,9 +738,9 @@ func TestPlanContinueAppliesOnlyToWaitingTransition(t *testing.T) { } // Same fingerprint without new approval must re-prompt (grant consumed). - var prompts int32 + var prompts atomic.Int32 g.opts.EmitPrompt = func(ctx context.Context, taskID string, pending PendingProposal, failure *FailureEvent) (string, error) { - atomic.AddInt32(&prompts, 1) + prompts.Add(1) go func() { time.Sleep(5 * time.Millisecond) _ = g.Resolve("c2", ActionContinue, "") @@ -751,7 +751,7 @@ func TestPlanContinueAppliesOnlyToWaitingTransition(t *testing.T) { if err != nil || !dec.Allow || !dec.AuthorizePlanReplacement { t.Fatalf("second continue = %+v %v", dec, err) } - if atomic.LoadInt32(&prompts) != 1 { + if prompts.Load() != 1 { t.Fatalf("expected re-prompt after fingerprint consumption") } } @@ -1061,7 +1061,7 @@ func TestRestoreDropsStalePendingAuthorization(t *testing.T) { func TestRestoreNeverRearmsActiveLocks(t *testing.T) { args := json.RawMessage(`{"path":"a.go","content":"x"}`) goal := NewGate(Options{}) - for i := 0; i < 3; i++ { + for range 3 { goal.ObserveResult(context.Background(), Observation{ TaskScopeID: "goal:ship", Tool: "write_file", Subject: "a.go", Mutates: true, Args: args, ErrSummary: "fail", @@ -1088,7 +1088,7 @@ func TestRestoreNeverRearmsActiveLocks(t *testing.T) { } turn := NewGate(Options{}) - for i := 0; i < 3; i++ { + for range 3 { turn.ObserveResult(context.Background(), Observation{ TaskScopeID: "turn:1", Tool: "write_file", Subject: "a.go", Mutates: true, Args: args, ErrSummary: "fail", @@ -1194,7 +1194,7 @@ func TestEpisodeTotalFailuresHardStopAcrossFingerprints(t *testing.T) { g := NewGate(Options{Reviewer: staticReviewer{ReviewVerdict{ Outcome: ReviewContinue, ChangeKind: ChangeSameStrategy, }}}) - for i := 0; i < MaxEpisodeFailures; i++ { + for i := range MaxEpisodeFailures { cmd := fmt.Sprintf("go test ./pkg%d", i) g.ObserveResult(context.Background(), Observation{ Tool: "bash", Subject: cmd, Verification: true, @@ -1224,19 +1224,19 @@ func TestEpisodeBudgetIsSharedAcrossSubagentTaskIDs(t *testing.T) { Outcome: ReviewContinue, ChangeKind: ChangeSameStrategy, }}}) // Split the Episode failure budget across root and two sub-agents. - for i := 0; i < 2; i++ { + for i := range 2 { g.ObserveResult(context.Background(), Observation{ TaskID: "root", Tool: "bash", Subject: fmt.Sprintf("root-%d", i), Verification: true, Args: json.RawMessage(fmt.Sprintf(`{"command":"root %d"}`, i)), ErrSummary: "fail", }) } - for i := 0; i < 2; i++ { + for i := range 2 { g.ObserveResult(context.Background(), Observation{ TaskID: "subagent:a", Tool: "bash", Subject: fmt.Sprintf("a-%d", i), Verification: true, Args: json.RawMessage(fmt.Sprintf(`{"command":"a %d"}`, i)), ErrSummary: "fail", }) } - for i := 0; i < 2; i++ { + for i := range 2 { g.ObserveResult(context.Background(), Observation{ TaskID: "subagent:b", Tool: "bash", Subject: fmt.Sprintf("b-%d", i), Verification: true, Args: json.RawMessage(fmt.Sprintf(`{"command":"b %d"}`, i)), ErrSummary: "fail", @@ -1300,7 +1300,7 @@ func TestStoppedOperationRetriesEscalateToEpisodeStop(t *testing.T) { Outcome: ReviewContinue, ChangeKind: ChangeSameStrategy, }}}) args := json.RawMessage(`{"command":"mvn test"}`) - for i := 0; i < MaxOperationFailures; i++ { + for range MaxOperationFailures { g.ObserveResult(context.Background(), Observation{ Tool: "bash", Subject: "mvn test", Verification: true, Args: args, ErrSummary: "fail", }) @@ -1323,7 +1323,7 @@ func TestSuccessfulMutationResetsEpisodeBudgets(t *testing.T) { g := NewGate(Options{Reviewer: staticReviewer{ReviewVerdict{ Outcome: ReviewContinue, ChangeKind: ChangeSameStrategy, }}}) - for i := 0; i < 4; i++ { + for range 4 { g.ObserveResult(context.Background(), Observation{ Tool: "bash", Subject: "go test", Verification: true, Args: json.RawMessage(`{"command":"go test"}`), ErrSummary: "fail", diff --git a/internal/recovery/persist_test.go b/internal/recovery/persist_test.go index b269b3b0fb..a27abe59c2 100644 --- a/internal/recovery/persist_test.go +++ b/internal/recovery/persist_test.go @@ -89,7 +89,7 @@ func TestSaveSnapshotIsAtomicAndOwnerOnly(t *testing.T) { sessionPath := filepath.Join(t.TempDir(), "session.jsonl") const writers = 24 var wg sync.WaitGroup - for i := 0; i < writers; i++ { + for i := range writers { wg.Add(1) go func(i int) { defer wg.Done() diff --git a/internal/recovery/reviewer.go b/internal/recovery/reviewer.go index f339552a7b..6cd00dd1c3 100644 --- a/internal/recovery/reviewer.go +++ b/internal/recovery/reviewer.go @@ -237,7 +237,7 @@ func buildReviewEvidence(failure *FailureEvent, diagnosis []string, proposal Pro // Drop order prefers keeping failure identity and proposal identity over large // excerpts (task summary → diagnosis notes → output → preview → args). func marshalEvidenceWithinBudget(ev reviewEvidence) ([]byte, error) { - for attempt := 0; attempt < 12; attempt++ { + for range 12 { raw, err := json.Marshal(ev) if err != nil { return nil, fmt.Errorf("marshal recovery evidence: %w", err) diff --git a/internal/recovery/reviewer_test.go b/internal/recovery/reviewer_test.go index d07958ae77..b14fffd5cd 100644 --- a/internal/recovery/reviewer_test.go +++ b/internal/recovery/reviewer_test.go @@ -228,7 +228,7 @@ func TestReviewerOutputBudgetAborts(t *testing.T) { go func() { defer close(ch) // Stream more than 4 KiB of text. - for i := 0; i < 20; i++ { + for range 20 { select { case <-ctx.Done(): return diff --git a/internal/recovery/state.go b/internal/recovery/state.go index c7c3adabc3..2ab291bb2c 100644 --- a/internal/recovery/state.go +++ b/internal/recovery/state.go @@ -2,6 +2,7 @@ package recovery import ( "encoding/json" + "slices" "strings" "unicode/utf8" ) @@ -309,8 +310,8 @@ func trimDiagnosis(af *activeFailure) { total := 0 kept := make([]string, 0, len(notes)) // Keep the newest notes within the total budget. - for i := len(notes) - 1; i >= 0; i-- { - n := clipDiagnosisNote(notes[i]) + for _, v := range slices.Backward(notes) { + n := clipDiagnosisNote(v) if n == "" { continue } @@ -341,10 +342,8 @@ func appendDiagnosisNote(af *activeFailure, note string) bool { if note == "" { return false } - for _, existing := range af.diagnosis { - if existing == note { - return false - } + if slices.Contains(af.diagnosis, note) { + return false } af.diagnosis = append(af.diagnosis, note) trimDiagnosis(af) diff --git a/internal/releaseasset/cli.go b/internal/releaseasset/cli.go index e9993a3bd5..dca96109aa 100644 --- a/internal/releaseasset/cli.go +++ b/internal/releaseasset/cli.go @@ -108,7 +108,7 @@ func fetchBounded(ctx context.Context, client *http.Client, rawURL string, limit func verifyChecksum(data []byte, assetName string, checksums []byte) error { want := "" - for _, line := range strings.Split(string(checksums), "\n") { + for line := range strings.SplitSeq(string(checksums), "\n") { fields := strings.Fields(line) if len(fields) != 2 || strings.TrimPrefix(fields[1], "*") != assetName { continue diff --git a/internal/remote/auth.go b/internal/remote/auth.go index 239d39a988..8b6ebd0d80 100644 --- a/internal/remote/auth.go +++ b/internal/remote/auth.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "strings" "sync" @@ -135,12 +136,7 @@ func publicKeyAuthCallback(methods []ssh.AuthMethod) ssh.ClientAuthCallback { } func containsAuthMethod(methods []string, want string) bool { - for _, method := range methods { - if method == want { - return true - } - } - return false + return slices.Contains(methods, want) } func agentAuth(identityFiles []string, identitiesOnly bool) (ssh.AuthMethod, func()) { diff --git a/internal/remote/bootstrap/bootstrap.go b/internal/remote/bootstrap/bootstrap.go index 1efebb10b7..9f25aebcc9 100644 --- a/internal/remote/bootstrap/bootstrap.go +++ b/internal/remote/bootstrap/bootstrap.go @@ -387,8 +387,8 @@ func resolveWorkspace(ctx context.Context, fs *sftpfs.FS, workspace, home string if workspace == "~" { return home, nil } - if strings.HasPrefix(workspace, "~/") { - return strings.TrimRight(home, "/") + "/" + strings.TrimPrefix(workspace, "~/"), nil + if after, ok0 := strings.CutPrefix(workspace, "~/"); ok0 { + return strings.TrimRight(home, "/") + "/" + after, nil } if strings.HasPrefix(workspace, "/") { return workspace, nil diff --git a/internal/remote/bootstrap/platform.go b/internal/remote/bootstrap/platform.go index 7d858faa03..0aedf66c5e 100644 --- a/internal/remote/bootstrap/platform.go +++ b/internal/remote/bootstrap/platform.go @@ -37,7 +37,7 @@ func ParseUname(out string) (goos, goarch string, err error) { // ParseVersion extracts a semver-ish string from `reasonix --version` output // like "reasonix v1.9.0" or "1.9.0". func ParseVersion(out string) (string, error) { - for _, field := range strings.Fields(strings.TrimSpace(out)) { + for field := range strings.FieldsSeq(strings.TrimSpace(out)) { v := strings.TrimPrefix(field, "v") if looksLikeSemver(v) { return v, nil diff --git a/internal/remote/dial.go b/internal/remote/dial.go index d60daf2c75..da34b73093 100644 --- a/internal/remote/dial.go +++ b/internal/remote/dial.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net" + "slices" "time" "golang.org/x/crypto/ssh" @@ -175,7 +176,7 @@ func handshakeTimeout(dialTimeout time.Duration) time.Duration { } func closeAll(clients []*ssh.Client) { - for i := len(clients) - 1; i >= 0; i-- { - _ = clients[i].Close() + for _, v := range slices.Backward(clients) { + _ = v.Close() } } diff --git a/internal/remote/reconnect_test.go b/internal/remote/reconnect_test.go index f98b9ae5d4..ec1108371e 100644 --- a/internal/remote/reconnect_test.go +++ b/internal/remote/reconnect_test.go @@ -3,6 +3,7 @@ package remote import ( "context" "math/rand" + "slices" "sync" "testing" "time" @@ -190,17 +191,12 @@ func snapshot(mu *sync.Mutex, s *[]Status) []Status { } func containsStatus(states []Status, want Status) bool { - for _, s := range states { - if s == want { - return true - } - } - return false + return slices.Contains(states, want) } func waitForWaiters(t *testing.T, c *fakeClock, n int) { t.Helper() - for i := 0; i < 200; i++ { + for range 200 { if c.pendingWaiters() >= n { return } diff --git a/internal/remote/remote.go b/internal/remote/remote.go index 02b755034c..bd1c779911 100644 --- a/internal/remote/remote.go +++ b/internal/remote/remote.go @@ -188,7 +188,7 @@ func (p BackoffPolicy) max() time.Duration { func (p BackoffPolicy) delay(attempt int) time.Duration { d := float64(p.initial()) f := p.factor() - for i := 0; i < attempt; i++ { + for range attempt { d *= f if d >= float64(p.max()) { return p.max() diff --git a/internal/remote/sftpfs/detect.go b/internal/remote/sftpfs/detect.go index d63d984189..a1bae9828a 100644 --- a/internal/remote/sftpfs/detect.go +++ b/internal/remote/sftpfs/detect.go @@ -1,6 +1,9 @@ package sftpfs -import "unicode/utf8" +import ( + "slices" + "unicode/utf8" +) // Kind classifies file content for preview purposes. type Kind int @@ -26,10 +29,8 @@ func DetectKind(sample []byte) Kind { if len(sample) > sniffLen { sample = sample[:sniffLen] } - for _, b := range sample { - if b == 0 { - return KindBinary - } + if slices.Contains(sample, 0) { + return KindBinary } if utf8.Valid(sample) { return KindText diff --git a/internal/remote/sshtest/sshtest.go b/internal/remote/sshtest/sshtest.go index 71912b439f..2bf3d04bc1 100644 --- a/internal/remote/sshtest/sshtest.go +++ b/internal/remote/sshtest/sshtest.go @@ -146,11 +146,9 @@ func (s *Server) serve() { s.mu.Lock() s.conns = append(s.conns, nConn) s.mu.Unlock() - s.wg.Add(1) - go func() { - defer s.wg.Done() + s.wg.Go(func() { s.handleConn(nConn) - }() + }) } } diff --git a/internal/repair/config_test.go b/internal/repair/config_test.go index f7ce5bc8de..7423fd3e66 100644 --- a/internal/repair/config_test.go +++ b/internal/repair/config_test.go @@ -270,7 +270,7 @@ func TestConfigSnapshotsRotateAndVerifyHash(t *testing.T) { home := t.TempDir() t.Setenv("REASONIX_HOME", home) path := filepath.Join(home, "config.toml") - for i := 0; i < configSnapshotRetention+2; i++ { + for i := range configSnapshotRetention + 2 { content := []byte("default_model = \"model-" + string(rune('a'+i)) + "\"\n") if err := os.WriteFile(path, content, 0o600); err != nil { t.Fatal(err) @@ -468,7 +468,7 @@ func TestPruneConfigSnapshotPreservesRecreatedMetadata(t *testing.T) { func recordConfigSnapshotSeries(t *testing.T, count int) []ConfigSnapshot { t.Helper() - for i := 0; i < count; i++ { + for i := range count { content := []byte("default_model = \"model-" + string(rune('a'+i)) + "\"\n") now := time.Date(2026, time.July, 29, 0, 0, i, 0, time.UTC) if err := recordConfigSnapshot(config.UserConfigPath(), content, "v1", now); err != nil { diff --git a/internal/repair/mutation_lock.go b/internal/repair/mutation_lock.go index dcfc8125a6..e3ca660957 100644 --- a/internal/repair/mutation_lock.go +++ b/internal/repair/mutation_lock.go @@ -8,6 +8,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "sort" "strings" "sync" @@ -103,7 +104,7 @@ func removeRepairNodeIfMatching(path, identityPath, expectedStateID string) erro } func moveRepairNodeToUniqueCleanup(path string) (string, error) { - for attempt := 0; attempt < 16; attempt++ { + for attempt := range 16 { cleanup := fmt.Sprintf("%s.reasonix-cleanup-%d-%d", path, time.Now().UTC().UnixNano(), attempt) err := renameRepairNodeNoReplace(path, cleanup) if err == nil { @@ -171,8 +172,8 @@ func resolveParentSymlinkPath(path string) string { if resolved, err := filepath.EvalSymlinks(dir); err == nil { parts := make([]string, 0, 1+len(missing)) parts = append(parts, resolved) - for i := len(missing) - 1; i >= 0; i-- { - parts = append(parts, missing[i]) + for _, v := range slices.Backward(missing) { + parts = append(parts, v) } return filepath.Join(parts...) } @@ -263,8 +264,8 @@ func lockRepairMutationsTimeout(timeout time.Duration, paths ...string) (func(), lockPath := filepath.Join(lockDir, fmt.Sprintf("%x.lock", digest)) release, err := filelock.Acquire(ctx, lockPath) if err != nil { - for i := len(releases) - 1; i >= 0; i-- { - releases[i]() + for _, v := range slices.Backward(releases) { + v() } return nil, fmt.Errorf("lock repair mutations: %w", err) } @@ -274,8 +275,8 @@ func lockRepairMutationsTimeout(timeout time.Duration, paths ...string) (func(), var once sync.Once return func() { once.Do(func() { - for i := len(releases) - 1; i >= 0; i-- { - releases[i]() + for _, v := range slices.Backward(releases) { + v() } }) }, nil diff --git a/internal/repair/transaction.go b/internal/repair/transaction.go index f109654cf5..c0a2feb7b3 100644 --- a/internal/repair/transaction.go +++ b/internal/repair/transaction.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "reflect" + "slices" "strings" "time" @@ -548,8 +549,8 @@ func UndoLastRepair() (*RepairTransaction, error) { tx.Changes[i].Undone = true return persistRepairTransaction(tx) } - for i := len(tx.Changes) - 1; i >= 0; i-- { - change := tx.Changes[i] + for i, v := range slices.Backward(tx.Changes) { + change := v if change.Undone { // Progress was persisted but the backup removal may have been cut // short by a crash; finish the cleanup the completed step owed. diff --git a/internal/repair/update.go b/internal/repair/update.go index d92256b386..b6d8020aa1 100644 --- a/internal/repair/update.go +++ b/internal/repair/update.go @@ -1457,7 +1457,7 @@ func quarantineExistingAppBundleUpdateBackup(tx *UpdateTransaction) (string, str if err != nil { return "", "", fmt.Errorf("read existing handoff backup digest: %w", err) } - for attempt := 0; attempt < 16; attempt++ { + for attempt := range 16 { quarantine := fmt.Sprintf( "%s.reasonix-orphaned-%d-%d", tx.BackupPath, @@ -2807,7 +2807,7 @@ func rollbackPendingUpdateMatchingLocked( } func retainUpdateRollbackNode(path, suffix string) (string, error) { - for attempt := 0; attempt < 16; attempt++ { + for attempt := range 16 { retained := fmt.Sprintf( "%s.%s-%d-%d", path, @@ -3161,7 +3161,7 @@ func stageUpdateRollbackBackup( file UpdateTransactionFile, mode os.FileMode, ) (string, string, error) { - for attempt := 0; attempt < 16; attempt++ { + for attempt := range 16 { stage := fmt.Sprintf( "%s.reasonix-rollback-stage-%d-%d", file.TargetPath, diff --git a/internal/repair/update_legacy.go b/internal/repair/update_legacy.go index c900109b4a..4141365501 100644 --- a/internal/repair/update_legacy.go +++ b/internal/repair/update_legacy.go @@ -188,7 +188,7 @@ func archiveSupersededAppBundleBackup(tx *UpdateTransaction, transactionID strin shortID = shortID[:16] } base := fmt.Sprintf("%s.reasonix-retired-%s-%s", tx.BackupPath, shortID, time.Now().UTC().Format("20060102T150405.000000000Z")) - for attempt := 0; attempt < 16; attempt++ { + for attempt := range 16 { archive := fmt.Sprintf("%s-%d", base, attempt) if err := renameRepairNodeNoReplace(tx.BackupPath, archive); err != nil { if os.IsExist(err) { @@ -223,7 +223,7 @@ func archiveSupersededPendingMarker(tx *UpdateTransaction, transactionID, kind s shortID = shortID[:16] } base := filepath.Join(archiveDir, fmt.Sprintf("%s-%s-%s", time.Now().UTC().Format("20060102T150405.000000000Z"), shortID, kind)) - for attempt := 0; attempt < 16; attempt++ { + for attempt := range 16 { archivePath := fmt.Sprintf("%s-%d.json", base, attempt) if err := renameRepairNodeNoReplace(pendingPath, archivePath); err != nil { if os.IsExist(err) { @@ -306,7 +306,7 @@ func ArchiveSupersededPendingFileUpdate(runningVersion, installRoot string) (boo } archiveBase := filepath.Join(archiveDir, fmt.Sprintf("%s-%s", time.Now().UTC().Format("20060102T150405.000000000Z"), shortID)) supersededUpdateBeforeArchive(pendingPath) - for attempt := 0; attempt < 16; attempt++ { + for attempt := range 16 { archivePath := fmt.Sprintf("%s-%d.json", archiveBase, attempt) if err := renameRepairNodeNoReplace(pendingPath, archivePath); err != nil { if os.IsExist(err) { diff --git a/internal/retrieval/bm25.go b/internal/retrieval/bm25.go index 0074687019..0d70cbf35d 100644 --- a/internal/retrieval/bm25.go +++ b/internal/retrieval/bm25.go @@ -178,17 +178,11 @@ func snippetAround(text string, byteIdx, maxRunes int) string { } runes := []rune(text) pos := utf8.RuneCountInString(text[:byteIdx]) - start := pos - maxRunes/2 - if start < 0 { - start = 0 - } + start := max(pos-maxRunes/2, 0) end := start + maxRunes if end > len(runes) { end = len(runes) - start = end - maxRunes - if start < 0 { - start = 0 - } + start = max(end-maxRunes, 0) } prefix := "" suffix := "" diff --git a/internal/sandbox/seatbelt_darwin_test.go b/internal/sandbox/seatbelt_darwin_test.go index 61d97f65b3..44cf2fca12 100644 --- a/internal/sandbox/seatbelt_darwin_test.go +++ b/internal/sandbox/seatbelt_darwin_test.go @@ -4,6 +4,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "strings" "testing" ) @@ -62,13 +63,7 @@ func TestWriteAllowDirsIncludesTemp(t *testing.T) { dirs := writeAllowDirs(nil) tmpDir := os.TempDir() realTmp, _ := filepath.EvalSymlinks(tmpDir) - found := false - for _, d := range dirs { - if d == realTmp { - found = true - break - } - } + found := slices.Contains(dirs, realTmp) if !found { t.Errorf("writeAllowDirs should include temp dir %s, got %v", tmpDir, dirs) } @@ -78,13 +73,7 @@ func TestWriteAllowDirsIncludesSessionTemp(t *testing.T) { private := t.TempDir() dirs := writeAllowDirsForSpec(Spec{SessionTemp: private, MinimalWrites: true}) real, _ := filepath.EvalSymlinks(private) - found := false - for _, d := range dirs { - if d == real { - found = true - break - } - } + found := slices.Contains(dirs, real) if !found { t.Fatalf("SessionTemp must be allowed under Seatbelt even with MinimalWrites: %v", dirs) } @@ -176,12 +165,7 @@ func containsDarwinPath(paths []string, want string) bool { if real, err := filepath.EvalSymlinks(abs); err == nil { abs = real } - for _, path := range paths { - if path == abs { - return true - } - } - return false + return slices.Contains(paths, abs) } func TestCommandUnwrappedWhenOff(t *testing.T) { diff --git a/internal/sandbox/seatbelt_other.go b/internal/sandbox/seatbelt_other.go index d6e7bc4ea6..e005f75684 100644 --- a/internal/sandbox/seatbelt_other.go +++ b/internal/sandbox/seatbelt_other.go @@ -207,7 +207,7 @@ func bwrapExecutableMountArgs(args []string) []string { } out := make([]string, 0, 2*strings.Count(rel, string(filepath.Separator))+4) current := "/tmp" - for _, part := range strings.Split(rel, string(filepath.Separator)) { + for part := range strings.SplitSeq(rel, string(filepath.Separator)) { if part == "" || part == "." { continue } diff --git a/internal/sandbox/seatbelt_other_test.go b/internal/sandbox/seatbelt_other_test.go index c24a68bb68..7606541853 100644 --- a/internal/sandbox/seatbelt_other_test.go +++ b/internal/sandbox/seatbelt_other_test.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "reflect" + "slices" "testing" ) @@ -122,10 +123,5 @@ func containsPath(paths []string, want string) bool { if err != nil { return false } - for _, p := range paths { - if p == absWant { - return true - } - } - return false + return slices.Contains(paths, absWant) } diff --git a/internal/sandbox/session_temp_env.go b/internal/sandbox/session_temp_env.go index 383ecf52d7..a36e277e7e 100644 --- a/internal/sandbox/session_temp_env.go +++ b/internal/sandbox/session_temp_env.go @@ -35,7 +35,7 @@ func SessionTempEnvMap(sessionTemp string, linuxSandboxed bool) map[string]strin } out := make(map[string]string, len(pairs)) for _, kv := range pairs { - for i := 0; i < len(kv); i++ { + for i := range len(kv) { if kv[i] == '=' { out[kv[:i]] = kv[i+1:] break diff --git a/internal/secrets/redact_test.go b/internal/secrets/redact_test.go index 7409beaf13..70eaf38924 100644 --- a/internal/secrets/redact_test.go +++ b/internal/secrets/redact_test.go @@ -39,7 +39,7 @@ func TestRedactMasksCommonSecretShapes(t *testing.T) { func TestRedactLongConcurrentTranscriptAvoidsRegexpBacktracking(t *testing.T) { const secret = "sk-real-secret-value-1234567890" var transcript strings.Builder - for i := 0; i < 2_000; i++ { + for i := range 2_000 { fmt.Fprintf(&transcript, "message %d payload=%s DEEPSEEK_API_KEY=%s Authorization: Bearer %s\n", i, strings.Repeat("x", i%31), secret, secret) } input := transcript.String() @@ -48,11 +48,9 @@ func TestRedactLongConcurrentTranscriptAvoidsRegexpBacktracking(t *testing.T) { const iterations = 20 var wg sync.WaitGroup errs := make(chan string, workers) - for worker := 0; worker < workers; worker++ { - wg.Add(1) - go func() { - defer wg.Done() - for i := 0; i < iterations; i++ { + for range workers { + wg.Go(func() { + for range iterations { got := Redact(input) if strings.Contains(got, secret) { errs <- "long concurrent redaction leaked the test secret" @@ -63,7 +61,7 @@ func TestRedactLongConcurrentTranscriptAvoidsRegexpBacktracking(t *testing.T) { return } } - }() + }) } wg.Wait() close(errs) diff --git a/internal/serve/auth.go b/internal/serve/auth.go index fbb5597ef5..1549f80745 100644 --- a/internal/serve/auth.go +++ b/internal/serve/auth.go @@ -489,11 +489,11 @@ func (ag *authGate) verifySession(token string) bool { } // Check expiry (format: "unix_timestamp|base64nonce"). - pipe := strings.IndexByte(payload, '|') - if pipe < 0 { + before, _, ok := strings.Cut(payload, "|") + if !ok { return false } - expiry, err := strconv.ParseInt(payload[:pipe], 10, 64) + expiry, err := strconv.ParseInt(before, 10, 64) if err != nil { return false } @@ -531,7 +531,7 @@ func generateToken() string { // acceptsHTML reports whether the request's Accept header prefers text/html. func acceptsHTML(r *http.Request) bool { - for _, h := range strings.Fields(r.Header.Get("Accept")) { + for h := range strings.FieldsSeq(r.Header.Get("Accept")) { if strings.HasPrefix(h, "text/html") { return true } @@ -546,8 +546,8 @@ func acceptsHTML(r *http.Request) bool { func (ag *authGate) clientIP(r *http.Request) string { if ag.behindProxy { if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" { - if i := strings.IndexByte(fwd, ','); i >= 0 { - return strings.TrimSpace(fwd[:i]) + if before, _, ok := strings.Cut(fwd, ","); ok { + return strings.TrimSpace(before) } return strings.TrimSpace(fwd) } diff --git a/internal/serve/auth_test.go b/internal/serve/auth_test.go index e38143eb95..9d9d0d62f8 100644 --- a/internal/serve/auth_test.go +++ b/internal/serve/auth_test.go @@ -118,7 +118,7 @@ func TestTokenModeValidCookie(t *testing.T) { }))) defer ts.Close() - req, _ := http.NewRequest("GET", ts.URL+"/status", nil) + req, _ := http.NewRequest(http.MethodGet, ts.URL+"/status", nil) req.AddCookie(&http.Cookie{Name: cookieToken, Value: "secret"}) resp, err := http.DefaultClient.Do(req) if err != nil { @@ -137,7 +137,7 @@ func TestTokenModeInvalidCookie(t *testing.T) { }))) defer ts.Close() - req, _ := http.NewRequest("GET", ts.URL+"/status", nil) + req, _ := http.NewRequest(http.MethodGet, ts.URL+"/status", nil) req.AddCookie(&http.Cookie{Name: cookieToken, Value: "wrong"}) resp, err := http.DefaultClient.Do(req) if err != nil { @@ -227,7 +227,7 @@ func TestTokenModeNonLoopbackCookieAllowsPlainHTTP(t *testing.T) { return http.ErrUseLastResponse }, } - req, _ := http.NewRequest("GET", ts.URL+"/?token=secret", nil) + req, _ := http.NewRequest(http.MethodGet, ts.URL+"/?token=secret", nil) req.Host = "192.0.2.10:8787" resp, err := client.Do(req) if err != nil { @@ -305,7 +305,7 @@ func TestPasswordModeNoSessionRedirects(t *testing.T) { return http.ErrUseLastResponse }, } - req, _ := http.NewRequest("GET", ts.URL+"/", nil) + req, _ := http.NewRequest(http.MethodGet, ts.URL+"/", nil) req.Header.Set("Accept", "text/html,application/xhtml+xml") resp, err := client.Do(req) if err != nil { @@ -329,7 +329,7 @@ func TestPasswordModeAPIWithoutSessionReturns401(t *testing.T) { defer ts.Close() // Simulate a non-browser (fetch) request by not setting Accept: text/html. - req, _ := http.NewRequest("GET", ts.URL+"/status", nil) + req, _ := http.NewRequest(http.MethodGet, ts.URL+"/status", nil) resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) @@ -376,7 +376,7 @@ func TestPasswordModeValidLogin(t *testing.T) { } // Use the session cookie to access a protected page. - req, _ := http.NewRequest("GET", ts.URL+"/status", nil) + req, _ := http.NewRequest(http.MethodGet, ts.URL+"/status", nil) req.AddCookie(sessionCookie) resp2, err := http.DefaultClient.Do(req) if err != nil { @@ -401,7 +401,7 @@ func TestPasswordModeNonLoopbackHTTPLoginCookieIsUsable(t *testing.T) { }, } form := url.Values{"password": {"correct"}} - req, _ := http.NewRequest("POST", ts.URL+"/login", strings.NewReader(form.Encode())) + req, _ := http.NewRequest(http.MethodPost, ts.URL+"/login", strings.NewReader(form.Encode())) req.Host = "192.0.2.10:8787" req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp, err := client.Do(req) @@ -418,7 +418,7 @@ func TestPasswordModeNonLoopbackHTTPLoginCookieIsUsable(t *testing.T) { t.Fatal("plain HTTP password session cookie should stay usable without Secure") } - req, _ = http.NewRequest("GET", ts.URL+"/status", nil) + req, _ = http.NewRequest(http.MethodGet, ts.URL+"/status", nil) req.Host = "192.0.2.10:8787" req.AddCookie(sessionCookie) resp, err = http.DefaultClient.Do(req) @@ -444,7 +444,7 @@ func TestPasswordModeSanitizesRedirectCookie(t *testing.T) { }, } form := url.Values{"password": {"correct"}} - req, _ := http.NewRequest("POST", ts.URL+"/login", strings.NewReader(form.Encode())) + req, _ := http.NewRequest(http.MethodPost, ts.URL+"/login", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.AddCookie(&http.Cookie{Name: cookieRedirect, Value: "//evil.example/path"}) @@ -601,7 +601,7 @@ func TestRateLimiterAllowsFiveThenBlocks(t *testing.T) { rl := newRateLimit() ip := "192.0.2.1" - for i := 0; i < rateLimitMax; i++ { + for i := range rateLimitMax { if !rl.allow(ip) { t.Fatalf("attempt %d should be allowed", i+1) } @@ -616,7 +616,7 @@ func TestRateLimiterResetsAfterWindow(t *testing.T) { ip := "192.0.2.2" // Exhaust the limit. - for i := 0; i < rateLimitMax; i++ { + for range rateLimitMax { rl.allow(ip) } if rl.allow(ip) { @@ -640,7 +640,7 @@ func TestRateLimiterResetsAfterWindow(t *testing.T) { func TestRateLimiterDifferentIPs(t *testing.T) { rl := newRateLimit() // Exhaust one IP. - for i := 0; i < rateLimitMax; i++ { + for range rateLimitMax { rl.allow("192.0.2.1") } // Another IP should still be allowed. @@ -653,7 +653,7 @@ func TestRateLimiterDifferentIPs(t *testing.T) { func TestClientIPRemoteAddr(t *testing.T) { ag := newAuthGate(config.ServeConfig{AuthMode: "token", Token: "x"}) - req, _ := http.NewRequest("GET", "/", nil) + req, _ := http.NewRequest(http.MethodGet, "/", nil) req.RemoteAddr = "192.0.2.42:12345" if got := ag.clientIP(req); got != "192.0.2.42" { t.Errorf("clientIP = %q, want 192.0.2.42", got) @@ -662,7 +662,7 @@ func TestClientIPRemoteAddr(t *testing.T) { func TestClientIPIgnoresXForwardedForWithoutProxy(t *testing.T) { ag := newAuthGate(config.ServeConfig{AuthMode: "token", Token: "x"}) - req, _ := http.NewRequest("GET", "/", nil) + req, _ := http.NewRequest(http.MethodGet, "/", nil) req.RemoteAddr = "192.0.2.1:12345" req.Header.Set("X-Forwarded-For", "10.0.0.1, 10.0.0.2") if got := ag.clientIP(req); got != "192.0.2.1" { @@ -672,7 +672,7 @@ func TestClientIPIgnoresXForwardedForWithoutProxy(t *testing.T) { func TestClientIPTrustsXForwardedForWithProxy(t *testing.T) { ag := newAuthGate(config.ServeConfig{AuthMode: "token", Token: "x", BehindProxy: true}) - req, _ := http.NewRequest("GET", "/", nil) + req, _ := http.NewRequest(http.MethodGet, "/", nil) req.RemoteAddr = "10.0.0.99:12345" req.Header.Set("X-Forwarded-For", "192.0.2.42, 10.0.0.1") if got := ag.clientIP(req); got != "192.0.2.42" { @@ -684,7 +684,7 @@ func TestClientIPTrustsXForwardedForWithProxy(t *testing.T) { func TestIsTLS(t *testing.T) { ag := newAuthGate(config.ServeConfig{AuthMode: "token", Token: "x"}) - req, _ := http.NewRequest("GET", "/", nil) + req, _ := http.NewRequest(http.MethodGet, "/", nil) if ag.isTLS(req) { t.Error("plain request should not be TLS") } @@ -692,7 +692,7 @@ func TestIsTLS(t *testing.T) { func TestIsTLSIgnoresForwardedProtoWithoutProxy(t *testing.T) { ag := newAuthGate(config.ServeConfig{AuthMode: "token", Token: "x"}) - req, _ := http.NewRequest("GET", "/", nil) + req, _ := http.NewRequest(http.MethodGet, "/", nil) req.Header.Set("X-Forwarded-Proto", "https") if ag.isTLS(req) { t.Error("should ignore X-Forwarded-Proto without behind_proxy") @@ -701,7 +701,7 @@ func TestIsTLSIgnoresForwardedProtoWithoutProxy(t *testing.T) { func TestIsTLSTrustsForwardedProtoWithProxy(t *testing.T) { ag := newAuthGate(config.ServeConfig{AuthMode: "token", Token: "x", BehindProxy: true}) - req, _ := http.NewRequest("GET", "/", nil) + req, _ := http.NewRequest(http.MethodGet, "/", nil) req.Header.Set("X-Forwarded-Proto", "https") if !ag.isTLS(req) { t.Error("should trust X-Forwarded-Proto with behind_proxy") @@ -711,19 +711,19 @@ func TestIsTLSTrustsForwardedProtoWithProxy(t *testing.T) { func TestAuthCookieSecurePolicy(t *testing.T) { ag := newAuthGate(config.ServeConfig{AuthMode: "token", Token: "x"}) for _, host := range []string{"localhost:8787", "127.0.0.1:8787", "[::1]:8787"} { - req, _ := http.NewRequest("GET", "http://"+host+"/", nil) + req, _ := http.NewRequest(http.MethodGet, "http://"+host+"/", nil) if ag.authCookieSecure(req) { t.Errorf("loopback host %s should allow local HTTP cookies", host) } } - req, _ := http.NewRequest("GET", "http://192.0.2.10:8787/", nil) + req, _ := http.NewRequest(http.MethodGet, "http://192.0.2.10:8787/", nil) if ag.authCookieSecure(req) { t.Fatal("plain HTTP cookies should stay usable without Secure") } proxy := newAuthGate(config.ServeConfig{AuthMode: "token", Token: "x", BehindProxy: true}) - req, _ = http.NewRequest("GET", "http://example.test/", nil) + req, _ = http.NewRequest(http.MethodGet, "http://example.test/", nil) req.Header.Set("X-Forwarded-Proto", "https") if !proxy.authCookieSecure(req) { t.Fatal("trusted forwarded HTTPS should mark cookies Secure") diff --git a/internal/serve/broadcaster_test.go b/internal/serve/broadcaster_test.go index 58804f18d7..76c25ea3f0 100644 --- a/internal/serve/broadcaster_test.go +++ b/internal/serve/broadcaster_test.go @@ -67,7 +67,7 @@ func TestBroadcasterDropsSlowSubscriber(t *testing.T) { ch, cancel := b.Subscribe() defer cancel() // Overfill far past the 64-slot buffer without reading; Emit must not block. - for i := 0; i < 1000; i++ { + for range 1000 { b.Emit(event.Event{Kind: event.Text, Text: "x"}) } if len(ch) == 0 { diff --git a/internal/serve/modelswitch_test.go b/internal/serve/modelswitch_test.go index c0ed347249..78a85c7c60 100644 --- a/internal/serve/modelswitch_test.go +++ b/internal/serve/modelswitch_test.go @@ -15,19 +15,15 @@ func TestControllerAccessorIsRaceSafe(t *testing.T) { s := &Server{ctrl: a} var wg sync.WaitGroup - for i := 0; i < 64; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range 64 { + wg.Go(func() { if got := s.ctl(); got != a && got != b { t.Errorf("ctl() returned a pointer that was never set") } - }() + }) } - for i := 0; i < 16; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range 16 { + wg.Go(func() { s.mu.Lock() if s.ctrl == a { s.ctrl = b @@ -35,7 +31,7 @@ func TestControllerAccessorIsRaceSafe(t *testing.T) { s.ctrl = a } s.mu.Unlock() - }() + }) } wg.Wait() } diff --git a/internal/serve/serve_lock_test.go b/internal/serve/serve_lock_test.go index 378da25c3c..1a11129a06 100644 --- a/internal/serve/serve_lock_test.go +++ b/internal/serve/serve_lock_test.go @@ -3,6 +3,7 @@ package serve import ( "context" "io" + "net/http" "net/http/httptest" "strings" "sync/atomic" @@ -266,7 +267,7 @@ func TestSubmitWaitsForExtensionReloadAndTargetsReplacement(t *testing.T) { submitDone := make(chan int, 1) go func() { - req := httptest.NewRequest("POST", "/submit", strings.NewReader(`{"input":"hello"}`)) + req := httptest.NewRequest(http.MethodPost, "/submit", strings.NewReader(`{"input":"hello"}`)) rec := httptest.NewRecorder() s.submit(rec, req) submitDone <- rec.Code diff --git a/internal/serve/serve_portfile_test.go b/internal/serve/serve_portfile_test.go index 4547f09cf2..34b1938dea 100644 --- a/internal/serve/serve_portfile_test.go +++ b/internal/serve/serve_portfile_test.go @@ -28,7 +28,7 @@ func waitForHTTP(t *testing.T, addr string) { t.Helper() client := &http.Client{Timeout: 2 * time.Second} var lastErr error - for i := 0; i < 100; i++ { + for range 100 { resp, err := client.Get("http://" + addr + "/assets/logo-wordmark.svg") if err == nil { resp.Body.Close() diff --git a/internal/serve/serve_test.go b/internal/serve/serve_test.go index e9a245afcc..7cd6ef2e70 100644 --- a/internal/serve/serve_test.go +++ b/internal/serve/serve_test.go @@ -154,11 +154,11 @@ func TestServeEndpoints(t *testing.T) { srv := httptest.NewServer(New(ctrl, bc, config.ServeConfig{}).Handler()) defer srv.Close() - if resp, err := http.Get(srv.URL + "/history"); err != nil || resp.StatusCode != 200 { + if resp, err := http.Get(srv.URL + "/history"); err != nil || resp.StatusCode != http.StatusOK { t.Fatalf("history = %v / %v", resp, err) } - if resp, _ := http.Get(srv.URL + "/context"); resp.StatusCode != 200 { + if resp, _ := http.Get(srv.URL + "/context"); resp.StatusCode != http.StatusOK { t.Errorf("context status = %d", resp.StatusCode) } @@ -405,7 +405,7 @@ func TestServeIndexPage(t *testing.T) { t.Fatal(err) } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { t.Errorf("index status = %d", resp.StatusCode) } ct := resp.Header.Get("Content-Type") @@ -985,7 +985,7 @@ func TestServeContextEndpoint(t *testing.T) { t.Fatal(err) } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { t.Errorf("context status = %d", resp.StatusCode) } var body map[string]int diff --git a/internal/sessiontemp/manager_test.go b/internal/sessiontemp/manager_test.go index f1b4527102..c3581151e3 100644 --- a/internal/sessiontemp/manager_test.go +++ b/internal/sessiontemp/manager_test.go @@ -226,11 +226,9 @@ func TestConcurrentAcquireRotateReleaseRace(t *testing.T) { defer m.Release() var wg sync.WaitGroup - for i := 0; i < 32; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < 50; j++ { + for range 32 { + wg.Go(func() { + for j := range 50 { lease, err := m.Acquire() if err != nil { t.Errorf("acquire: %v", err) @@ -242,7 +240,7 @@ func TestConcurrentAcquireRotateReleaseRace(t *testing.T) { } lease.Release() } - }() + }) } wg.Wait() } diff --git a/internal/shellparse/bash.go b/internal/shellparse/bash.go index 950519aa4c..b6893384ac 100644 --- a/internal/shellparse/bash.go +++ b/internal/shellparse/bash.go @@ -297,7 +297,7 @@ func wordHasUnescapedBrace(word *syntax.Word) bool { func hasUnescapedMeta(value, meta string) bool { escaped := false - for i := 0; i < len(value); i++ { + for i := range len(value) { if escaped { escaped = false continue @@ -547,7 +547,7 @@ func IsAssignment(word string) bool { if !ok || name == "" { return false } - for i := 0; i < len(name); i++ { + for i := range len(name) { c := name[i] if i == 0 { if c != '_' && (c < 'A' || c > 'Z') && (c < 'a' || c > 'z') { diff --git a/internal/shellrun/runner_test.go b/internal/shellrun/runner_test.go index 52ce847f56..36febe8745 100644 --- a/internal/shellrun/runner_test.go +++ b/internal/shellrun/runner_test.go @@ -224,7 +224,7 @@ func TestRunForegroundPreservesInterleaving(t *testing.T) { } argv := shellArgvWith(sh, "for i in 1 2 3 4 5 6 7 8; do echo out$i; echo err$i 1>&2; done") // Repeat: two pipes reorder probabilistically, so one run can pass by luck. - for run := 0; run < 10; run++ { + for run := range 10 { res := RunForeground(context.Background(), Request{Argv: argv, Timeout: 30 * time.Second}) if res.Combined != want.String() { t.Fatalf("run %d lost child write order:\ngot %q\nwant %q", run, res.Combined, want.String()) diff --git a/internal/shellsafe/bash_redirect.go b/internal/shellsafe/bash_redirect.go index 167cafc500..701b2478bb 100644 --- a/internal/shellsafe/bash_redirect.go +++ b/internal/shellsafe/bash_redirect.go @@ -114,7 +114,7 @@ func isSafeFDDupWord(source string, word *syntax.Word) bool { if value == "" { return false } - for i := 0; i < len(value); i++ { + for i := range len(value) { if value[i] < '0' || value[i] > '9' { return false } diff --git a/internal/skill/builtins_test.go b/internal/skill/builtins_test.go index 700697a441..071fb9bf51 100644 --- a/internal/skill/builtins_test.go +++ b/internal/skill/builtins_test.go @@ -3,6 +3,7 @@ package skill import ( "context" "encoding/json" + "slices" "strings" "testing" @@ -100,13 +101,7 @@ func TestBuiltinSkillsIncludeCodeGraphHintAndToolsWhenDiscovered(t *testing.T) { t.Fatalf("explore body missing priority hint %q:\n%s", want, explore.Body) } } - found := false - for _, name := range explore.AllowedTools { - if name == "mcp__codegraph__symbols" { - found = true - break - } - } + found := slices.Contains(explore.AllowedTools, "mcp__codegraph__symbols") if !found { t.Fatalf("explore allowed tools = %v, want codegraph tool", explore.AllowedTools) } diff --git a/internal/skill/skill.go b/internal/skill/skill.go index 1b1b69fa50..16a6aeace8 100644 --- a/internal/skill/skill.go +++ b/internal/skill/skill.go @@ -19,6 +19,7 @@ import ( "os" "path" "path/filepath" + "slices" "sort" "strings" @@ -514,12 +515,7 @@ func normalizePluginPaths(paths map[string][]string) map[string][]string { } func stringSliceContains(items []string, want string) bool { - for _, item := range items { - if item == want { - return true - } - } - return false + return slices.Contains(items, want) } // Roots exposes the discovery directories with their status for `/skill paths`. @@ -1019,12 +1015,7 @@ func frontmatterHasSkillMarkerKey(content string) bool { } func isSkillMarkerFrontmatterKey(key string) bool { - for _, marker := range skillMarkerFrontmatterKeys { - if key == marker { - return true - } - } - return false + return slices.Contains(skillMarkerFrontmatterKeys, key) } // Create scaffolds a new skill stub at the chosen scope. Refuses to overwrite. @@ -1291,7 +1282,7 @@ func parseCSVFrontmatter(raw string) []string { raw = strings.TrimSpace(raw[1 : len(raw)-1]) } var out []string - for _, p := range strings.Split(raw, ",") { + for p := range strings.SplitSeq(raw, ",") { if t := strings.Trim(strings.TrimSpace(p), `"'`); t != "" { out = append(out, t) } diff --git a/internal/skill/skill_test.go b/internal/skill/skill_test.go index 4777a8a8cf..39b3469b65 100644 --- a/internal/skill/skill_test.go +++ b/internal/skill/skill_test.go @@ -811,12 +811,7 @@ func sameStrings(a, b []string) bool { } func containsString(ss []string, want string) bool { - for _, s := range ss { - if s == want { - return true - } - } - return false + return slices.Contains(ss, want) } func TestInvalidNamesSkipped(t *testing.T) { @@ -1044,7 +1039,7 @@ func TestManualInvocationSkillExcludedFromIndex(t *testing.T) { func TestApplyIndexTruncates(t *testing.T) { var skills []Skill - for i := 0; i < 200; i++ { + for range 200 { skills = append(skills, Skill{Name: "skill" + strings.Repeat("x", 20), Description: strings.Repeat("d", 50)}) } out := ApplyIndex("BASE", skills) diff --git a/internal/skill/tools.go b/internal/skill/tools.go index d7b2e58011..46e65f96aa 100644 --- a/internal/skill/tools.go +++ b/internal/skill/tools.go @@ -659,7 +659,7 @@ func cleanSkillName(raw string) string { return "" } stripped := strings.TrimSpace(bracketTagRe.ReplaceAllString(raw, " ")) - for _, tok := range strings.Fields(stripped) { + for tok := range strings.FieldsSeq(stripped) { if c := tok[0]; (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') { return tok } diff --git a/internal/stats/query.go b/internal/stats/query.go index a82149bbb3..71444bbf82 100644 --- a/internal/stats/query.go +++ b/internal/stats/query.go @@ -1,6 +1,7 @@ package stats import ( + "maps" "sort" "strings" "time" @@ -144,9 +145,7 @@ func (w *Writer) Query(f SourceFilter) (RangeStats, error) { // timeline; inactive days carry zero totals. The frontend trims the // left side of the chart on narrow containers instead of hiding days. byModel := make(map[string]int64, len(dayTotals)) - for k, v := range dayTotals { - byModel[k] = v - } + maps.Copy(byModel, dayTotals) byProvider := map[string]int64{} for m, v := range dayTotals { byProvider[providerOf(m)] += v diff --git a/internal/stats/stats_test.go b/internal/stats/stats_test.go index 8e158271bd..08ad6cd058 100644 --- a/internal/stats/stats_test.go +++ b/internal/stats/stats_test.go @@ -388,12 +388,12 @@ func TestConcurrentWritersAppendWholeRecords(t *testing.T) { const writers = 8 const perWriter = 40 var wg sync.WaitGroup - for i := 0; i < writers; i++ { + for i := range writers { wg.Add(1) go func(model int) { defer wg.Done() w := NewWriter(dir) - for j := 0; j < perWriter; j++ { + for range perWriter { if err := w.Append(record{Timestamp: now, ModelRef: fmt.Sprintf("provider/model-%d", model), Total: 1}); err != nil { t.Errorf("append: %v", err) return diff --git a/internal/sysproxy/sysproxy.go b/internal/sysproxy/sysproxy.go index 9a75b39286..87659e8a2d 100644 --- a/internal/sysproxy/sysproxy.go +++ b/internal/sysproxy/sysproxy.go @@ -19,9 +19,9 @@ func splitList(s string) []string { func parseProxyList(list, scheme string) *url.URL { var fallback string for _, f := range splitList(list) { - if i := strings.IndexByte(f, '='); i >= 0 { - if strings.EqualFold(f[:i], scheme) { - return hostProxyURL(f[i+1:]) + if before, after, ok := strings.Cut(f, "="); ok { + if strings.EqualFold(before, scheme) { + return hostProxyURL(after) } continue } diff --git a/internal/taskintent/heuristic.go b/internal/taskintent/heuristic.go index c7973779b0..5e33790048 100644 --- a/internal/taskintent/heuristic.go +++ b/internal/taskintent/heuristic.go @@ -1,6 +1,7 @@ package taskintent import ( + "slices" "strings" "unicode/utf8" ) @@ -28,10 +29,8 @@ func heuristicInputIsTask(input string) bool { words := strings.Fields(normalized) if len(words) <= 3 { - for _, greeting := range shortGreetings { - if normalized == greeting { - return false - } + if slices.Contains(shortGreetings, normalized) { + return false } } @@ -43,17 +42,17 @@ func heuristicInputIsTask(input string) bool { "谢谢你", "辛苦了", } for _, phrase := range chatPhrases { - index := strings.Index(normalized, phrase) - if index < 0 { + before, after, ok := strings.Cut(normalized, phrase) + if !ok { continue } // Acknowledgement wording only short-circuits a purely conversational // turn. Preserve a real task before it or after an explicit transition, // e.g. "thanks for fixing that; now update the tests". - if prefix := strings.TrimSpace(normalized[:index]); prefix != "" && heuristicInputHasStrongTaskSignal(prefix) { + if prefix := strings.TrimSpace(before); prefix != "" && heuristicInputHasStrongTaskSignal(prefix) { return true } - if deliveryTaskHasFollowUpAfterChat(normalized[index+len(phrase):]) { + if deliveryTaskHasFollowUpAfterChat(after) { return true } return false @@ -155,14 +154,9 @@ func containsTaskNeedle(input, needle string) bool { if containsNonASCII(needle) || strings.Contains(needle, " ") { return strings.Contains(input, needle) } - for _, word := range strings.FieldsFunc(input, func(r rune) bool { + return slices.Contains(strings.FieldsFunc(input, func(r rune) bool { return !(r >= 'a' && r <= 'z') && !(r >= '0' && r <= '9') && r != '_' - }) { - if word == needle { - return true - } - } - return false + }), needle) } func containsNonASCII(s string) bool { diff --git a/internal/taskintent/intent.go b/internal/taskintent/intent.go index bddaf2caa8..13ad5ab14d 100644 --- a/internal/taskintent/intent.go +++ b/internal/taskintent/intent.go @@ -406,8 +406,8 @@ func deliveryTaskStartsWithMutation(input string) bool { for { stripped := false for _, prefix := range []string{"please ", "can you ", "could you ", "would you ", "you should ", "帮我", "请你", "直接", "继续", "再"} { - if strings.HasPrefix(input, prefix) { - input = strings.TrimSpace(strings.TrimPrefix(input, prefix)) + if after, ok := strings.CutPrefix(input, prefix); ok { + input = strings.TrimSpace(after) stripped = true break } diff --git a/internal/taskmonitor/control.go b/internal/taskmonitor/control.go index fbdb94ae68..5e68deda36 100644 --- a/internal/taskmonitor/control.go +++ b/internal/taskmonitor/control.go @@ -277,7 +277,7 @@ func (cs *ControlService) controlOp(ctx context.Context, projectDir, taskID stri // heartbeat. Retry those expected version advances. If RecordDone already // persisted the requested terminal state, use that snapshot as the result. const maxControlSaveAttempts = 4 - for attempt := 0; attempt < maxControlSaveAttempts; attempt++ { + for attempt := range maxControlSaveAttempts { next := *snap next.Version++ next.State = targetState diff --git a/internal/taskmonitor/control_test.go b/internal/taskmonitor/control_test.go index 42a4fe7861..146eee4c5a 100644 --- a/internal/taskmonitor/control_test.go +++ b/internal/taskmonitor/control_test.go @@ -527,10 +527,7 @@ func TestControlService_ConcurrentKillersRemainCallScoped(t *testing.T) { {taskID: "task-a", sessionID: "session-a"}, {taskID: "task-b", sessionID: "session-b"}, } { - tc := tc - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { <-started killer := &mockKiller{fn: func(sessionID, taskID string) bool { killed <- sessionID + "/" + taskID @@ -540,7 +537,7 @@ func TestControlService_ConcurrentKillersRemainCallScoped(t *testing.T) { if err != nil || !res.Accepted { t.Errorf("StopTaskWithKiller(%s): result=%+v err=%v", tc.taskID, res, err) } - }() + }) } close(started) wg.Wait() @@ -572,17 +569,15 @@ func TestControlService_ConcurrentAccess(t *testing.T) { success := 0 var mu sync.Mutex - for i := 0; i < 10; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range 10 { + wg.Go(func() { res, _ := cs.StopTaskWithKiller(context.Background(), "/p", "t1", 1, "", "", killer) if res.Accepted { mu.Lock() success++ mu.Unlock() } - }() + }) } wg.Wait() // Exactly one caller should succeed due to mutex + version CAS diff --git a/internal/taskmonitor/jsonstore.go b/internal/taskmonitor/jsonstore.go index 49e3bbf8fb..6afb91e2a3 100644 --- a/internal/taskmonitor/jsonstore.go +++ b/internal/taskmonitor/jsonstore.go @@ -91,7 +91,7 @@ func rejectSymlinkChain(root, target string) error { if rel == "." { return nil } - for _, part := range strings.Split(rel, string(filepath.Separator)) { + for part := range strings.SplitSeq(rel, string(filepath.Separator)) { cur = filepath.Join(cur, part) if err := rejectSymlink(cur); err != nil { return err @@ -106,7 +106,7 @@ func rejectStoreParents(projectDir, root string) error { return err } cur := projectDir - for _, part := range strings.Split(rel, string(filepath.Separator)) { + for part := range strings.SplitSeq(rel, string(filepath.Separator)) { if part == "." || part == "" { continue } @@ -224,7 +224,7 @@ func (s *FileStore) RenewRuntimeLease(ctx context.Context, projectDir, taskID, o return false, nil } const maxAttempts = 4 - for attempt := 0; attempt < maxAttempts; attempt++ { + for range maxAttempts { snap, err := s.getTaskRaw(ctx, projectDir, taskID) if err != nil || snap == nil { return false, err @@ -476,7 +476,7 @@ func (s *FileStore) AppendAuditEvent(ctx context.Context, projectDir string, ev return err } max := 0 - for _, line := range strings.Split(string(raw), "\n") { + for line := range strings.SplitSeq(string(raw), "\n") { line = strings.TrimSpace(line) if line == "" { continue diff --git a/internal/taskmonitor/recorder.go b/internal/taskmonitor/recorder.go index a8573fe6a6..a5914e06bf 100644 --- a/internal/taskmonitor/recorder.go +++ b/internal/taskmonitor/recorder.go @@ -49,7 +49,7 @@ func newRuntimeOwnerID() string { if _, err := crand.Read(nonce[:]); err == nil { return hex.EncodeToString(nonce[:]) } - h := sha256.Sum256([]byte(fmt.Sprintf("%d:%d", timeNow().UnixNano(), runtimeOwnerSequence.Add(1)))) + h := sha256.Sum256(fmt.Appendf(nil, "%d:%d", timeNow().UnixNano(), runtimeOwnerSequence.Add(1))) return hex.EncodeToString(h[:16]) } @@ -74,7 +74,7 @@ func sessionlessMonitorTaskID(jobID string) string { if _, err := crand.Read(nonce[:]); err != nil { // crypto/rand failure is exceptional; retain a bounded, non-path-like // identity rather than falling back to the colliding raw job ID. - h := sha256.Sum256([]byte(fmt.Sprintf("%s:%d", jobID, timeNow().UnixNano()))) + h := sha256.Sum256(fmt.Appendf(nil, "%s:%d", jobID, timeNow().UnixNano())) return hex.EncodeToString(h[:8]) + "--" + jobID } return hex.EncodeToString(nonce[:]) + "--" + jobID @@ -181,7 +181,7 @@ func (r *TaskRecorder) RecordDone(id string, st jobs.Status, jobErr error) { } r.stopHeartbeat(monitorID) const maxSaveAttempts = 4 - for attempt := 0; attempt < maxSaveAttempts; attempt++ { + for range maxSaveAttempts { cur, gerr := r.store.GetTask(ctx, r.projectDir, monitorID) if gerr != nil || cur == nil { return // never recorded (recorder attached after the job started) diff --git a/internal/telemetry/client_test.go b/internal/telemetry/client_test.go index 87f481843b..0ef8cf5535 100644 --- a/internal/telemetry/client_test.go +++ b/internal/telemetry/client_test.go @@ -227,7 +227,7 @@ func TestPendingQueueCountsActiveAndRecoversStaleClaims(t *testing.T) { if err := os.MkdirAll(dir, 0o700); err != nil { t.Fatal(err) } - for i := 0; i < maxPending; i++ { + for i := range maxPending { path := filepath.Join(dir, strings.Repeat("a", 16)+"-"+time.Unix(int64(i), 0).Format("150405")+".json.uploading") if err := os.WriteFile(path, []byte("{}"), 0o600); err != nil { t.Fatal(err) diff --git a/internal/telemetry/sink.go b/internal/telemetry/sink.go index 93316077a5..d24a969605 100644 --- a/internal/telemetry/sink.go +++ b/internal/telemetry/sink.go @@ -6,6 +6,7 @@ import ( "net" "regexp" "runtime" + "slices" "strings" "time" @@ -217,10 +218,8 @@ func safeBucket(value, fallback string) string { func enumBucket(value string, allowed ...string) string { value = strings.ToLower(strings.TrimSpace(value)) - for _, item := range allowed { - if value == item { - return value - } + if slices.Contains(allowed, value) { + return value } return "other" } diff --git a/internal/tool/builtin/bash.go b/internal/tool/builtin/bash.go index 7fb3c23a14..d766be1f66 100644 --- a/internal/tool/builtin/bash.go +++ b/internal/tool/builtin/bash.go @@ -10,6 +10,7 @@ import ( "os/exec" "path/filepath" "runtime" + "slices" "strings" "sync" "time" @@ -629,7 +630,7 @@ func reapShellProcess(cmd *exec.Cmd, tracked *proc.TrackedCommand) { // PowerShell chaining guard. func hasUnquotedSeq(s, seq string) bool { var quote byte - for i := 0; i < len(s); i++ { + for i := range len(s) { c := s[i] if quote != 0 { if c == quote { @@ -748,9 +749,9 @@ func runShellPATHCommand(parent context.Context, shell string, args []string) [] func parseShellPATH(out []byte, marker string) string { lines := strings.Split(strings.ReplaceAll(string(out), "\r\n", "\n"), "\n") - for i := len(lines) - 1; i >= 0; i-- { - if strings.HasPrefix(lines[i], marker) { - return strings.TrimSpace(strings.TrimPrefix(lines[i], marker)) + for _, line := range slices.Backward(lines) { + if rest, ok := strings.CutPrefix(line, marker); ok { + return strings.TrimSpace(rest) } } return "" @@ -789,8 +790,8 @@ func setEnvValue(env []string, key, value string) []string { } func envValue(env []string, key string) (string, bool) { - for i := len(env) - 1; i >= 0; i-- { - k, v, ok := strings.Cut(env[i], "=") + for _, entry := range slices.Backward(env) { + k, v, ok := strings.Cut(entry, "=") if ok && envKeyEqual(k, key) { return v, true } diff --git a/internal/tool/builtin/bash_cancel_windows_test.go b/internal/tool/builtin/bash_cancel_windows_test.go index 379c8376c8..1bd0a7f6dc 100644 --- a/internal/tool/builtin/bash_cancel_windows_test.go +++ b/internal/tool/builtin/bash_cancel_windows_test.go @@ -54,7 +54,7 @@ func TestBashCancelKillsWindowsChildProcessTree(t *testing.T) { if err == nil { t.Fatal("expected cancel to return an error") } - for i := 0; i < 50; i++ { + for range 50 { if !windowsProcessAlive(childPID) { return } @@ -87,7 +87,7 @@ func TestBashWindowsReapsChildAfterForegroundShellExit(t *testing.T) { killWindowsPID(childPID) t.Fatalf("foreground command failed: %v (out=%q)", err, out) } - for i := 0; i < 50; i++ { + for range 50 { if !windowsProcessAlive(childPID) { return } @@ -129,7 +129,7 @@ func TestBashCancelKillsGitBashHereDocPython(t *testing.T) { killWindowsPID(childPID) t.Fatal("cancel did not interrupt Git Bash here-doc python within 20s") } - for i := 0; i < 50; i++ { + for range 50 { if !windowsProcessAlive(childPID) { return } diff --git a/internal/tool/builtin/bash_reap_test.go b/internal/tool/builtin/bash_reap_test.go index 25149dddc3..d824673b9e 100644 --- a/internal/tool/builtin/bash_reap_test.go +++ b/internal/tool/builtin/bash_reap_test.go @@ -46,7 +46,7 @@ func TestReapTreeKillsGroupStragglers(t *testing.T) { proc.KillTree(cmd) dead := false - for i := 0; i < 50; i++ { + for range 50 { if syscall.Kill(pid, 0) != nil { dead = true break diff --git a/internal/tool/builtin/bash_sandbox_escape_test.go b/internal/tool/builtin/bash_sandbox_escape_test.go index d66c2aba6c..147459baa6 100644 --- a/internal/tool/builtin/bash_sandbox_escape_test.go +++ b/internal/tool/builtin/bash_sandbox_escape_test.go @@ -213,11 +213,11 @@ func windowsSandboxFailureForShell(sh sandbox.Shell) string { func backgroundJobIDFromStartOutput(t *testing.T, out string) string { t.Helper() const prefix = `Started background job "` - start := strings.Index(out, prefix) - if start < 0 { + _, after, ok := strings.Cut(out, prefix) + if !ok { t.Fatalf("start output = %q, want background job id", out) } - rest := out[start+len(prefix):] + rest := after end := strings.IndexByte(rest, '"') if end < 0 { t.Fatalf("start output = %q, want closing quote for background job id", out) diff --git a/internal/tool/builtin/bgjobs.go b/internal/tool/builtin/bgjobs.go index 742677dde7..226bd75e2c 100644 --- a/internal/tool/builtin/bgjobs.go +++ b/internal/tool/builtin/bgjobs.go @@ -85,7 +85,7 @@ func filterLines(s, re string) (string, error) { return "", fmt.Errorf("invalid filter regexp: %w", err) } var keep []string - for _, line := range strings.Split(s, "\n") { + for line := range strings.SplitSeq(s, "\n") { if rx.MatchString(line) { keep = append(keep, line) } diff --git a/internal/tool/builtin/encoding_helpers.go b/internal/tool/builtin/encoding_helpers.go index b4d26ccbeb..941b5fd917 100644 --- a/internal/tool/builtin/encoding_helpers.go +++ b/internal/tool/builtin/encoding_helpers.go @@ -3,6 +3,7 @@ package builtin import ( "fmt" "os" + "slices" "strings" fileenc "reasonix/internal/fileutil/encoding" @@ -353,8 +354,8 @@ func stripReadFileLinePrefix(line string) (string, bool) { func replaceEditRanges(content string, ranges []editRange, replacement string) string { updated := content - for i := len(ranges) - 1; i >= 0; i-- { - r := ranges[i] + for _, v := range slices.Backward(ranges) { + r := v updated = updated[:r.start] + replacement + updated[r.end:] } return updated @@ -433,11 +434,8 @@ func firstNonEmptyLine(s string) string { } func commonPrefixLen(a, b string) int { - n := len(a) - if len(b) < n { - n = len(b) - } - for i := 0; i < n; i++ { + n := min(len(b), len(a)) + for i := range n { if a[i] != b[i] { return i } diff --git a/internal/tool/builtin/gitignore_e2e_test.go b/internal/tool/builtin/gitignore_e2e_test.go index 60eac3707c..3c1984626f 100644 --- a/internal/tool/builtin/gitignore_e2e_test.go +++ b/internal/tool/builtin/gitignore_e2e_test.go @@ -87,7 +87,7 @@ func TestGrepWorkspaceE2E(t *testing.T) { // files, relative to repo with forward slashes. func matchedRelFiles(repo, out string) map[string]bool { found := map[string]bool{} - for _, line := range strings.Split(out, "\n") { + for line := range strings.SplitSeq(out, "\n") { i := strings.LastIndex(line, ":") if i < 0 { continue diff --git a/internal/tool/builtin/managed_config.go b/internal/tool/builtin/managed_config.go index fe5eb979e8..c1f049d1db 100644 --- a/internal/tool/builtin/managed_config.go +++ b/internal/tool/builtin/managed_config.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "slices" "strings" "reasonix/internal/tool" @@ -48,12 +49,7 @@ func (m ManagedConfigPaths) Match(target string) bool { if err != nil { return false } - for _, p := range m.paths { - if abs == p { - return true - } - } - return false + return slices.Contains(m.paths, abs) } // approve asks the user whether this managed-config write may proceed, via the diff --git a/internal/tool/builtin/post_write_receipt.go b/internal/tool/builtin/post_write_receipt.go index a652b9b02a..71a78786ce 100644 --- a/internal/tool/builtin/post_write_receipt.go +++ b/internal/tool/builtin/post_write_receipt.go @@ -37,13 +37,7 @@ func renderPostWriteReceipts(receipts []editReplacementReceipt) string { // Share the bounded body between the selected first/last receipts and their // matched/replacement fields. The final clip below remains a defensive cap // for unusually large counts or labels. - fieldBudget := (maxPostWriteReceiptBytes - 256) / (2 * len(indexes)) - if fieldBudget < 128 { - fieldBudget = 128 - } - if fieldBudget > maxCapturedReceiptSpanBytes { - fieldBudget = maxCapturedReceiptSpanBytes - } + fieldBudget := min(max((maxPostWriteReceiptBytes-256)/(2*len(indexes)), 128), maxCapturedReceiptSpanBytes) var b strings.Builder b.Grow(maxPostWriteReceiptBytes) diff --git a/internal/tool/builtin/post_write_receipt_test.go b/internal/tool/builtin/post_write_receipt_test.go index f03ed96d8d..26d5f2c7a0 100644 --- a/internal/tool/builtin/post_write_receipt_test.go +++ b/internal/tool/builtin/post_write_receipt_test.go @@ -52,7 +52,7 @@ func TestPostWriteReceiptLargeSpanAllocationIsBounded(t *testing.T) { receipts := []editReplacementReceipt{{matched: large, replacement: large, occurrences: 1}} var got string result := testing.Benchmark(func(b *testing.B) { - for i := 0; i < b.N; i++ { + for range b.N { got = withActualPostWriteReceipts("edited large.txt", receipts) } }) diff --git a/internal/tool/builtin/readfile_stream_test.go b/internal/tool/builtin/readfile_stream_test.go index 296f2b68f5..4fc95e1b13 100644 --- a/internal/tool/builtin/readfile_stream_test.go +++ b/internal/tool/builtin/readfile_stream_test.go @@ -18,7 +18,7 @@ func TestReadFileStreamsLargeGB18030(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "big.gbk") var sb strings.Builder - for i := 0; i < 20000; i++ { + for range 20000 { sb.WriteString("第一行中文 line one 你好世界\n") } sb.WriteString("终点标记 THE-END\n") @@ -45,7 +45,7 @@ func TestReadFileLargeBoundedMemory(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "big.txt") var sb strings.Builder - for i := 0; i < 130000; i++ { // ~8 MB, no NUL + for range 130000 { // ~8 MB, no NUL sb.WriteString("a line of perfectly ordinary text in a large utf-8 file\n") } if err := os.WriteFile(path, []byte(sb.String()), 0o644); err != nil { diff --git a/internal/tool/builtin/tool_extra_test.go b/internal/tool/builtin/tool_extra_test.go index 13a5e7e38d..4d22f670f9 100644 --- a/internal/tool/builtin/tool_extra_test.go +++ b/internal/tool/builtin/tool_extra_test.go @@ -172,7 +172,7 @@ func TestGrepTruncation(t *testing.T) { dir := t.TempDir() // Create a file with many matching lines. var b strings.Builder - for i := 0; i < 300; i++ { + for i := range 300 { fmt.Fprintf(&b, "match %d\n", i) } os.WriteFile(filepath.Join(dir, "many.txt"), []byte(b.String()), 0o644) diff --git a/internal/tool/registry_canon_test.go b/internal/tool/registry_canon_test.go index 97e396ea5c..58d858033e 100644 --- a/internal/tool/registry_canon_test.go +++ b/internal/tool/registry_canon_test.go @@ -35,7 +35,7 @@ func TestSchemasCanonicalizesOncePerTool(t *testing.T) { t.Fatalf("Schema() called %d times at Add, want 1", calls) } - for i := 0; i < 50; i++ { + for range 50 { schemas := r.Schemas() if len(schemas) != 1 { t.Fatalf("Schemas() returned %d entries, want 1", len(schemas)) diff --git a/internal/tool/sessiontool/sessiontool.go b/internal/tool/sessiontool/sessiontool.go index 8cd02ceede..6f44623e7d 100644 --- a/internal/tool/sessiontool/sessiontool.go +++ b/internal/tool/sessiontool/sessiontool.go @@ -212,14 +212,14 @@ func truncateRunes(s string, max int) string { func modelFromPath(path string) string { name := filepath.Base(path) name = strings.TrimSuffix(name, ".jsonl") - firstDash := strings.Index(name, "-") - if firstDash < 0 { + _, after, ok := strings.Cut(name, "-") + if !ok { return "(unknown)" } - rest := name[firstDash+1:] - secondDash := strings.Index(rest, "-") - if secondDash < 0 { + rest := after + _, after0, ok0 := strings.Cut(rest, "-") + if !ok0 { return rest } - return rest[secondDash+1:] + return after0 } diff --git a/internal/tool/sessiontool/sessiontool_test.go b/internal/tool/sessiontool/sessiontool_test.go index 1d2bafda17..a9dcfdca8d 100644 --- a/internal/tool/sessiontool/sessiontool_test.go +++ b/internal/tool/sessiontool/sessiontool_test.go @@ -187,7 +187,7 @@ func TestReadSession_RespectsMaxTurns(t *testing.T) { dir := t.TempDir() sessionPath := filepath.Join(dir, "session.jsonl") var msgs []provider.Message - for i := 0; i < 10; i++ { + for range 10 { msgs = append(msgs, provider.Message{Role: provider.RoleUser, Content: "turn"}, provider.Message{Role: provider.RoleAssistant, Content: "answer"}, @@ -210,7 +210,7 @@ func TestReadSession_MaxTurnsZeroNoLimit(t *testing.T) { dir := t.TempDir() sessionPath := filepath.Join(dir, "session.jsonl") var msgs []provider.Message - for i := 0; i < 60; i++ { + for range 60 { msgs = append(msgs, provider.Message{Role: provider.RoleUser, Content: "turn"}, provider.Message{Role: provider.RoleAssistant, Content: "answer"}, diff --git a/internal/tool/tool.go b/internal/tool/tool.go index 2d72d7adc1..5cbfeb0d0c 100644 --- a/internal/tool/tool.go +++ b/internal/tool/tool.go @@ -7,6 +7,7 @@ package tool import ( "context" "encoding/json" + "slices" "sort" "strings" "sync" @@ -414,11 +415,8 @@ func (r *Registry) ResolveCall(name string) (resolved Tool, canonical string, ca if !ok { continue } - for _, alias := range mcpBindingAliases(b) { - if name == alias { - matches[canonicalName] = t - break - } + if slices.Contains(mcpBindingAliases(b), name) { + matches[canonicalName] = t } } if len(matches) == 1 { diff --git a/internal/worktree/worktree.go b/internal/worktree/worktree.go index 68a4640682..e3245cc9dd 100644 --- a/internal/worktree/worktree.go +++ b/internal/worktree/worktree.go @@ -85,7 +85,7 @@ func Create(ctx context.Context, workspaceRoot, managedRoot string) (Result, err repoBase = "repository" } - for attempt := 0; attempt < 5; attempt++ { + for range 5 { id, randomErr := randomID() if randomErr != nil { return Result{}, randomErr diff --git a/tools/repolint/baseline.json b/tools/repolint/baseline.json index a7711b3a40..d6766300d1 100644 --- a/tools/repolint/baseline.json +++ b/tools/repolint/baseline.json @@ -3,11 +3,11 @@ "banner": 0, "commented-code": 0, "essay": 4111, - "file-size": 73754, + "file-size": 73519, "layering": 1, "marker": 0, "narrative": 66, - "test-file-size": 64807 + "test-file-size": 64753 }, "files": { "cmd/e2ebench/main.go": { @@ -27,14 +27,14 @@ }, "desktop/app.go": { "essay": 90, - "file-size": 11545 + "file-size": 11538 }, "desktop/app_autosave_test.go": { "essay": 1 }, "desktop/app_test.go": { "essay": 2, - "test-file-size": 10208 + "test-file-size": 10200 }, "desktop/bot_bridge.go": { "essay": 11 @@ -131,7 +131,7 @@ }, "desktop/remote_window_test.go": { "essay": 1, - "test-file-size": 129 + "test-file-size": 128 }, "desktop/runtime_rebuilt_event_test.go": { "essay": 1 @@ -166,7 +166,7 @@ }, "desktop/settings_app.go": { "essay": 28, - "file-size": 3014, + "file-size": 3013, "narrative": 1 }, "desktop/settings_app_test.go": { @@ -190,17 +190,17 @@ }, "desktop/tabs.go": { "essay": 123, - "file-size": 8822 + "file-size": 8804 }, "desktop/tabs_order_test.go": { - "test-file-size": 397 + "test-file-size": 395 }, "desktop/tabs_sink_context_test.go": { "essay": 2 }, "desktop/tabs_topic_test.go": { "essay": 1, - "test-file-size": 3425 + "test-file-size": 3421 }, "desktop/temphelper_test.go": { "essay": 4 @@ -291,7 +291,7 @@ }, "internal/acp/service.go": { "essay": 66, - "file-size": 2173 + "file-size": 2172 }, "internal/acp/service_lock_test.go": { "essay": 11, @@ -302,7 +302,7 @@ }, "internal/agent/agent.go": { "essay": 128, - "file-size": 3273 + "file-size": 3270 }, "internal/agent/ask.go": { "essay": 4 @@ -335,11 +335,11 @@ }, "internal/agent/coordinator.go": { "essay": 20, - "file-size": 291 + "file-size": 292 }, "internal/agent/coordinator_test.go": { "essay": 5, - "test-file-size": 1248 + "test-file-size": 1244 }, "internal/agent/delivery_hardening_test.go": { "essay": 5 @@ -429,11 +429,11 @@ }, "internal/agent/run_loop.go": { "essay": 45, - "file-size": 320 + "file-size": 311 }, "internal/agent/save.go": { "essay": 105, - "file-size": 1320 + "file-size": 1319 }, "internal/agent/save_test.go": { "essay": 9, @@ -489,7 +489,7 @@ }, "internal/agent/task.go": { "essay": 65, - "file-size": 1409 + "file-size": 1407 }, "internal/agent/task_test.go": { "essay": 4, @@ -503,7 +503,7 @@ }, "internal/agent/usecapability.go": { "essay": 9, - "file-size": 780 + "file-size": 777 }, "internal/agent/usecapability_test.go": { "test-file-size": 998 @@ -513,16 +513,16 @@ }, "internal/autoresearch/store.go": { "essay": 3, - "file-size": 223 + "file-size": 216 }, "internal/boot/boot.go": { "essay": 110, - "file-size": 2203, + "file-size": 2194, "narrative": 4 }, "internal/boot/boot_test.go": { "essay": 10, - "test-file-size": 4337 + "test-file-size": 4331 }, "internal/boot/extension_dispatch_test.go": { "essay": 3, @@ -577,20 +577,20 @@ }, "internal/bot/feishu/feishu.go": { "essay": 1, - "file-size": 211 + "file-size": 210 }, "internal/bot/gateway.go": { "essay": 28, - "file-size": 2113 + "file-size": 2109 }, "internal/bot/gateway_lock_test.go": { "essay": 8 }, "internal/bot/gateway_test.go": { - "test-file-size": 1700 + "test-file-size": 1698 }, "internal/bot/project_index.go": { - "file-size": 32 + "file-size": 31 }, "internal/bot/render.go": { "essay": 5 @@ -613,14 +613,14 @@ }, "internal/checkpoint/checkpoint.go": { "essay": 8, - "file-size": 314 + "file-size": 305 }, "internal/checkpoint/observer.go": { "essay": 1 }, "internal/checkpoint/transaction.go": { "essay": 2, - "file-size": 972 + "file-size": 973 }, "internal/cli/acp.go": { "essay": 7 @@ -636,18 +636,18 @@ }, "internal/cli/chat_tui.go": { "essay": 111, - "file-size": 4590 + "file-size": 4558 }, "internal/cli/chat_tui_paste.go": { "essay": 9 }, "internal/cli/chat_tui_test.go": { "essay": 8, - "test-file-size": 3590 + "test-file-size": 3582 }, "internal/cli/cli.go": { "essay": 60, - "file-size": 2008 + "file-size": 2004 }, "internal/cli/cli_test.go": { "essay": 9, @@ -655,7 +655,7 @@ }, "internal/cli/complete.go": { "essay": 13, - "file-size": 74 + "file-size": 49 }, "internal/cli/complete_test.go": { "test-file-size": 15 @@ -759,8 +759,7 @@ "essay": 2 }, "internal/cli/transcript.go": { - "essay": 1, - "file-size": 2 + "essay": 1 }, "internal/cli/upgrade.go": { "essay": 2 @@ -775,7 +774,7 @@ "essay": 6 }, "internal/config/backfill_test.go": { - "test-file-size": 945 + "test-file-size": 944 }, "internal/config/cache_policy.go": { "essay": 9 @@ -785,7 +784,7 @@ }, "internal/config/config.go": { "essay": 56, - "file-size": 1493, + "file-size": 1489, "narrative": 2 }, "internal/config/credentials.go": { @@ -806,7 +805,7 @@ }, "internal/config/edit.go": { "essay": 15, - "file-size": 1594 + "file-size": 1592 }, "internal/config/edit_test.go": { "test-file-size": 2194 @@ -819,7 +818,7 @@ }, "internal/config/load.go": { "essay": 24, - "file-size": 1200 + "file-size": 1196 }, "internal/config/main_test.go": { "essay": 1 @@ -831,7 +830,7 @@ "test-file-size": 499 }, "internal/config/migrate.go": { - "file-size": 10 + "file-size": 7 }, "internal/config/migrate_test.go": { "test-file-size": 331 @@ -875,12 +874,12 @@ }, "internal/control/controller.go": { "essay": 180, - "file-size": 5379, + "file-size": 5367, "narrative": 4 }, "internal/control/controller_test.go": { "essay": 10, - "test-file-size": 4516 + "test-file-size": 4513 }, "internal/control/errmsg.go": { "essay": 2 @@ -1003,7 +1002,7 @@ }, "internal/evidence/evidence.go": { "essay": 38, - "file-size": 2120 + "file-size": 2080 }, "internal/evidence/evidence_test.go": { "essay": 1, @@ -1033,7 +1032,7 @@ "narrative": 1 }, "internal/extension/dispatch/dispatch_test.go": { - "test-file-size": 311 + "test-file-size": 310 }, "internal/extension/dispatch/payloads.go": { "essay": 2 @@ -1147,7 +1146,7 @@ }, "internal/hook/hook.go": { "essay": 44, - "file-size": 838 + "file-size": 829 }, "internal/hook/hook_test.go": { "essay": 8, @@ -1199,7 +1198,7 @@ }, "internal/jobs/jobs.go": { "essay": 42, - "file-size": 1263 + "file-size": 1264 }, "internal/jobs/jobs_extra_test.go": { "essay": 1 @@ -1250,15 +1249,15 @@ }, "internal/plugin/lazy_test.go": { "essay": 19, - "test-file-size": 279 + "test-file-size": 275 }, "internal/plugin/plugin.go": { "essay": 30, - "file-size": 1324 + "file-size": 1315 }, "internal/plugin/plugin_test.go": { "essay": 10, - "test-file-size": 921 + "test-file-size": 918 }, "internal/plugin/security.go": { "essay": 3 @@ -1281,7 +1280,7 @@ }, "internal/plugin/transport_stdio.go": { "essay": 12, - "file-size": 3 + "file-size": 4 }, "internal/pluginpkg/claude_compat.go": { "essay": 19 @@ -1318,29 +1317,29 @@ }, "internal/provider/anthropic/anthropic_test.go": { "essay": 1, - "test-file-size": 222 + "test-file-size": 221 }, "internal/provider/openai/host.go": { "essay": 8 }, "internal/provider/openai/openai.go": { "essay": 42, - "file-size": 555 + "file-size": 554 }, "internal/provider/openai/openai_test.go": { "essay": 5, - "test-file-size": 1558 + "test-file-size": 1557 }, "internal/provider/openai/realcache_test.go": { "essay": 7 }, "internal/provider/provider.go": { "essay": 50, - "file-size": 350 + "file-size": 345 }, "internal/provider/responses/responses.go": { "essay": 26, - "file-size": 81 + "file-size": 78 }, "internal/provider/responses/responses_test.go": { "essay": 6, @@ -1518,10 +1517,10 @@ }, "internal/skill/skill.go": { "essay": 7, - "file-size": 633 + "file-size": 624 }, "internal/skill/skill_test.go": { - "test-file-size": 283 + "test-file-size": 278 }, "internal/skill/tools.go": { "essay": 2 @@ -1564,7 +1563,7 @@ }, "internal/tool/builtin/bash.go": { "essay": 14, - "file-size": 24 + "file-size": 25 }, "internal/tool/builtin/bash_env_test.go": { "essay": 4 diff --git a/tools/repolint/comments_test.go b/tools/repolint/comments_test.go index 23e750300b..00b2463fc2 100644 --- a/tools/repolint/comments_test.go +++ b/tools/repolint/comments_test.go @@ -1,6 +1,9 @@ package main -import "testing" +import ( + "strings" + "testing" +) func rules(t *testing.T, rel, src string) []string { t.Helper() @@ -57,14 +60,15 @@ func TestEssayWeightIsTheExcessOverTheLimit(t *testing.T) { func TestDocGoCarriesTheLongPackageExplanation(t *testing.T) { long := "package p\n" - doc := "// Package p explains itself.\n" + var doc strings.Builder + doc.WriteString("// Package p explains itself.\n") for range capPackageDoc + 2 { - doc += "//\n// more prose\n" + doc.WriteString("//\n// more prose\n") } - if got := rules(t, "sub/doc.go", doc+long); len(got) != 0 { + if got := rules(t, "sub/doc.go", doc.String()+long); len(got) != 0 { t.Fatalf("doc.go package comment flagged: %v", got) } - if got := rules(t, "sub/other.go", doc+long); len(got) == 0 { + if got := rules(t, "sub/other.go", doc.String()+long); len(got) == 0 { t.Fatal("same package comment outside doc.go should be flagged") } }