chore: apply go fix syntax upgrades (min/max, switch patterns)

Apply Go 1.21+ min/max builtins and improved switch pattern matching
across test files and utilities. No functional changes.
This commit is contained in:
viettranx committed 2026-04-15 11:24:57 +07:00
1 parent 77dfb97d89
commit 3b4004946e
51 files changed
+242 -280

No files matched your search

+28 -14
View File
@@ -188,25 +188,39 @@ Published to GHCR (`ghcr.io/nextlevelbuilder/goclaw`) and Docker Hub (`digitop/g
Apply before finalizing any multi-phase plan. Trust-but-verify between scout → planner → final plan.
### Verification discipline (what to verify)
1. **Verify factual claims against code** — re-grep/re-count every number, path, endpoint. Don't copy from scout summaries.
2. **Re-scout on scope change** — if phase promotes from deferred → active, re-scout. Don't reuse brainstorm summary.
3. **Grep delete scope deep** — `grep -rn '<symbol>' .` whole repo. Stubs often have refs in catalogs/routing/switch cases. Enumerate ALL sites in todo.
4. **Verify external API endpoints via `docs-seeker`** — before writing endpoint into plan. Sibling APIs often use different roots.
5. **Cross-phase gates explicit** — "Phase N-1 merged + tests green" in phase Context. Execution order alone ≠ enforcement.
6. **Scout desktop and web separately** — `ui/desktop/frontend/` ≠ `ui/web/`. Different structure, i18n namespaces, test framework presence.
7. **i18n keys ordering** — add key + 3 catalogs as explicit todo step BEFORE handler code. Missing key = runtime crash.
8. **Signature-change callers enumeration** — grep + list all callers explicitly. "Update all callers" insufficient.
9. **Zero-coverage characterization test = blocker step** — write byte/request-body fixture test BEFORE migration. Not "recommended".
10. **Context key style convention** — check existing `context.go` pattern before introducing new key types. Mixed = code smell.
11. **Alias/shim coverage** — enumerate ALL exported symbols via `go doc <pkg>`. Add compile-time signature guards.
12. **Verify pass MANDATORY after rewrite** — spawn fresh Explore/grep to audit planner output. Don't trust self-validation.
13. **No fabricated Go identifiers** — every type/function/package referenced in plan must be grep-verified to exist (`grep -rn '^func <name>\|^type <name>' <pkg>`). Plausible-sounding APIs (`Keyring`, `StartSpan`, `Validator`, `security.SSRF`) are RED FLAGS — conventions differ per codebase. Rule of thumb: if you can't cite `file_path:line_number` for a symbol, you're fabricating. Apply especially when planner says "reuse existing X" — re-verify X exists before writing into plan.
14. **Plausible-but-wrong API families** — watch for: OTel-style `StartSpan/EndSpan` (codebase may be emit-based); wrapper types like `Keyring/Vault/Manager` (codebase may use free functions); `internal/security` or `internal/auth` packages (often scattered instead of centralized). When unsure, `go doc <pkg>` lists actual exported surface.
2. **Trace semantics, not just cite lines** — when plan references existing/upstream code, identify WHEN each field mutates and under WHAT conditions. Line-range citation without control-flow trace = how ports silently invert behavior. Check: every call, or specific branches only?
3. **No fabricated identifiers / API families** — every symbol in plan must cite `file:line`. RED FLAGS: plausible-sounding wrappers (`Keyring`, `Validator`, `Manager`), centralized packages (`internal/security`, `internal/auth`) that may be scattered, OTel-style (`StartSpan/EndSpan`) when codebase is emit-based. When unsure, `go doc <pkg>` lists actual exported surface. Apply especially when plan says "reuse existing X".
4. **Struct scope audit before adding state** — verify lifetime (per-request/session/agent/process) before adding a field to an existing struct. "Plausibly per-X" is a red flag — grep construction + ownership. Shared-instance state leaks across isolation boundaries.
5. **Gate-premise test math** — before asserting "feature X triggers independently of Y", list all early-returns from function entry to X. Math-verify any fixture claiming "X without Y".
6. **Port = config-shape match** — "faithful port" divergences in config field name/type are silent breaking changes for users copying upstream config. Match upstream shape, or explicitly flag each divergence with rationale in the phase file.
7. **Verify external API endpoints via `docs-seeker`** — before writing endpoint into plan. Sibling APIs often use different roots.
### Scope & coverage (where to look)
8. **Grep delete scope deep** — `grep -rn '<symbol>' .` whole repo. Stubs often have refs in catalogs/routing/switch cases. Enumerate ALL sites in todo.
9. **Signature-change callers enumeration** — grep + list all callers explicitly. "Update all callers" insufficient.
10. **Alias/shim coverage** — enumerate ALL exported symbols via `go doc <pkg>`. Add compile-time signature guards.
11. **Scout desktop and web separately** — `ui/desktop/frontend/` ≠ `ui/web/`. Different structure, i18n namespaces, test framework presence.
### Phasing & ordering (when)
12. **Re-scout on scope change** — if phase promotes from deferred → active, re-scout. Don't reuse brainstorm summary.
13. **Cross-phase gates explicit** — "Phase N-1 merged + tests green" in phase Context. Execution order alone ≠ enforcement.
14. **Zero-coverage characterization test = blocker step** — write byte/request-body fixture test BEFORE migration. Not "recommended".
15. **i18n keys ordering** — add key + 3 catalogs as explicit todo step BEFORE handler code. Missing key = runtime crash.
### Conventions & finalization
16. **Context key style convention** — check existing `context.go` pattern before introducing new key types. Mixed = code smell.
17. **Verify pass MANDATORY after rewrite** — spawn fresh Explore/grep to audit planner output. Don't trust self-validation.
**Pattern to avoid:** user asks → planner writes → report "done".
**Safer pattern:** user asks → scout → planner writes → audit-verify → report.
**Concrete red-team practice:** After planner completes, run `code-reviewer`/`brainstormer` agent in audit mode with explicit instruction "spot-check 15+ factual claims against live codebase". Caught fabricated `internal/security/`, `crypto.Keyring`, `tracing.StartSpan` in Agent Hooks plan (see `plans/260414-2229-agent-hooks-system/reports/audit-260414-2310-plan-review.md`).
**Red-team practice:** After planner completes, run `code-reviewer`/`brainstormer` in audit mode: "spot-check 15+ claims vs live codebase". Past catches: fabricated `crypto.Keyring`/`tracing.StartSpan` (agent-hooks plan); inverted TS-port semantics + wrong struct scope + misread early-return gate (context-pruning plan). See `plans/*/reports/audit-*.md` for concrete examples.
## Post-Implementation Checklist
+2 -4
View File
@@ -129,16 +129,14 @@ func handleSubagentAnnounce(
// Enqueue into producer-consumer queue using tenant-scoped key from routing.
isProcessor := enqueueSubagentAnnounce(queueKey, entry)
if isProcessor {
deps.BgWg.Add(1)
go func() {
defer deps.BgWg.Done()
deps.BgWg.Go(func() {
defer safego.Recover(nil, "component", "subagent_announce_loop", "session", sessionKey)
// Fetch live roster for merged announce context.
roster := deps.SubagentMgr.RosterForParent(parentAgent)
processSubagentAnnounceLoop(ctx, routing, roster, deps.SubagentMgr, deps.Sched, deps.MsgBus, deps.Cfg)
}()
})
}
return true
+2
View File
@@ -45,6 +45,7 @@ func setupToolRegistry(
browserMgr *browser.Manager,
webFetchTool *tools.WebFetchTool,
ttsTool *tools.TtsTool,
audioMgr *audio.Manager,
permPE *permissions.PolicyEngine,
toolPE *tools.PolicyEngine,
dataDir string,
@@ -142,6 +143,7 @@ func setupToolRegistry(
}
setupAudioExtras(cfg, ttsMgr) // Phase 3: registers Music + SFX providers.
audio.BridgeLegacySTT(ttsMgr, cfg) // Phase 4: bridge per-channel STTProxyURL → channel-scoped providers.
audioMgr = ttsMgr // expose to caller for channel STT wiring (Phase 5)
// Audio generation tool — backed by audio.Manager (Music + SFX).
toolsReg.Register(tools.NewCreateAudioTool(ttsMgr))
+1 -1
View File
@@ -22,7 +22,7 @@ func testExecToolFromGatewaySetup(t *testing.T, workspace, dataDir string) *tool
cfg.Tools.Browser.Enabled = false
providerRegistry := providers.NewRegistry(nil)
toolsReg, _, _, _, _, _, _, _, _, _, _ := setupToolRegistry(cfg, workspace, providerRegistry)
toolsReg, _, _, _, _, _, _, _, _, _, _, _ := setupToolRegistry(cfg, workspace, providerRegistry)
execToolAny, ok := toolsReg.Get("exec")
if !ok {
+4 -4
View File
@@ -91,7 +91,7 @@ func BenchmarkSanitizeHistory_WithToolCalls(b *testing.B) {
}
// Repeat to make it longer
for i := 0; i < 20; i++ {
for range 20 {
msgs = append(msgs, msgs[:7]...)
}
@@ -118,7 +118,7 @@ func BenchmarkSanitizeHistory_WithMissingToolResults(b *testing.B) {
}
// Repeat to make it longer
for i := 0; i < 25; i++ {
for range 25 {
msgs = append(msgs, msgs[:5]...)
}
@@ -144,7 +144,7 @@ func BenchmarkSanitizeHistory_ConsecutiveSameRole(b *testing.B) {
}
// Repeat to make it longer
for i := 0; i < 30; i++ {
for range 30 {
msgs = append(msgs, msgs[:8]...)
}
@@ -161,7 +161,7 @@ func makeHistoryMessages(pairs int) []providers.Message {
msgs := make([]providers.Message, 0, pairs*2)
content := strings.Repeat("message content ", 10)
for i := 0; i < pairs; i++ {
for i := range pairs {
// Add user message
msgs = append(msgs, providers.Message{
Role: "user",
+2 -2
View File
@@ -137,7 +137,7 @@ func TestAbortRun_AlreadyAborting(t *testing.T) {
results := make([]AbortResult, n)
var wg sync.WaitGroup
wg.Add(n)
for i := 0; i < n; i++ {
for i := range n {
i := i
go func() {
defer wg.Done()
@@ -213,7 +213,7 @@ func TestAbortRun_AfterUnregister(t *testing.T) {
// TestAbortRun_Race_UnregisterConcurrent verifies no panic and no goroutine leak
// when UnregisterRun and AbortRun interleave across 100 iterations.
func TestAbortRun_Race_UnregisterConcurrent(t *testing.T) {
for i := 0; i < 100; i++ {
for range 100 {
r := NewRouter()
runID := "run-race"
sessionKey := "session-race"
+4 -6
View File
@@ -47,7 +47,7 @@ func TestVoiceCache_LRUEviction(t *testing.T) {
voices := []audio.Voice{{ID: "x"}}
// Fill to capacity
for i := 0; i < cap; i++ {
for i := range cap {
c.Set(ids[i], voices)
}
// Access ids[0] to make it recently used
@@ -83,15 +83,13 @@ func TestVoiceCache_Invalidate(t *testing.T) {
func TestVoiceCache_ConcurrentSafe(t *testing.T) {
c := audio.NewVoiceCache(time.Hour, 500)
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for range 100 {
wg.Go(func() {
tid := uuid.New()
c.Set(tid, []audio.Voice{{ID: "v"}})
c.Get(tid)
c.Invalidate(tid)
}()
})
}
wg.Wait()
}
+3 -3
View File
@@ -84,11 +84,11 @@ func PgDumpVersion(ctx context.Context) (string, error) {
// "pg_dump (PostgreSQL) 18.3" -> 18
func ParsePgDumpMajor(version string) int {
const marker = "(PostgreSQL) "
idx := strings.Index(version, marker)
if idx < 0 {
_, after, ok := strings.Cut(version, marker)
if !ok {
return 0
}
rest := version[idx+len(marker):]
rest := after
end := 0
for end < len(rest) && rest[end] >= '0' && rest[end] <= '9' {
end++
+3 -4
View File
@@ -169,10 +169,9 @@ func (c *InMemoryCache[V]) sweepOnce() {
sort.Slice(allAlive, func(i, j int) bool {
return allAlive[i].createdAt.Before(allAlive[j].createdAt)
})
toEvict := len(allAlive) - c.maxSize + (c.maxSize / 5) // bring below cap + 20% headroom
if toEvict > len(allAlive) {
toEvict = len(allAlive)
}
toEvict := min(
// bring below cap + 20% headroom
len(allAlive)-c.maxSize+(c.maxSize/5), len(allAlive))
for i := 0; i < toEvict; i++ {
c.data.Delete(allAlive[i].key)
}
+5 -5
View File
@@ -95,7 +95,7 @@ func TestInMemoryCache_Clear(t *testing.T) {
// the background sweep goroutine (not just lazy on Get).
func TestInMemoryCache_PeriodicSweep(t *testing.T) {
c := NewInMemoryCache[string](
WithSweepInterval[string](20*time.Millisecond),
WithSweepInterval[string](20 * time.Millisecond),
)
defer c.Close()
ctx := context.Background()
@@ -126,7 +126,7 @@ func TestInMemoryCache_MaxSizeEviction(t *testing.T) {
ctx := context.Background()
// Insert 10 entries with distinct creation times to ensure oldest-first ordering
for i := 0; i < 10; i++ {
for i := range 10 {
c.Set(ctx, string(rune('a'+i)), i, 0)
time.Sleep(2 * time.Millisecond)
}
@@ -142,7 +142,7 @@ func TestInMemoryCache_MaxSizeEviction(t *testing.T) {
// TestInMemoryCache_Close verifies Close stops the sweep goroutine (no leak).
func TestInMemoryCache_Close(t *testing.T) {
c := NewInMemoryCache[string](
WithSweepInterval[string](10*time.Millisecond),
WithSweepInterval[string](10 * time.Millisecond),
)
ctx := context.Background()
c.Set(ctx, "k", "v", 0)
@@ -168,13 +168,13 @@ func TestInMemoryCache_ConcurrentSweepAndSet(t *testing.T) {
done := make(chan bool)
go func() {
for i := 0; i < 500; i++ {
for i := range 500 {
c.Set(ctx, string(rune('a'+(i%26))), i, 5*time.Millisecond)
}
done <- true
}()
go func() {
for i := 0; i < 500; i++ {
for i := range 500 {
_, _ = c.Get(ctx, string(rune('a'+(i%26))))
}
done <- true
+1 -1
View File
@@ -189,7 +189,7 @@ var graphBackoffBase = 1 * time.Second
func (g *GraphClient) doRequest(ctx context.Context, method, path string, body any) ([]byte, error) {
apiURL := fmt.Sprintf("%s/%s%s", graphAPIBase, graphAPIVersion, path)
for attempt := 0; attempt < maxRetries; attempt++ {
for attempt := range maxRetries {
if attempt > 0 {
backoff := time.Duration(1<<uint(attempt-1)) * graphBackoffBase
select {
+6 -6
View File
@@ -11,7 +11,7 @@ type facebookCreds struct {
// facebookInstanceConfig holds non-secret config from channel_instances.config JSONB.
type facebookInstanceConfig struct {
PageID string `json:"page_id"`
PageID string `json:"page_id"`
Features struct {
CommentReply bool `json:"comment_reply"`
FirstInbox bool `json:"first_inbox"`
@@ -24,7 +24,7 @@ type facebookInstanceConfig struct {
MessengerOptions struct {
SessionTimeout string `json:"session_timeout"`
} `json:"messenger_options"`
PostContextCacheTTL string `json:"post_context_cache_ttl"`
PostContextCacheTTL string `json:"post_context_cache_ttl"`
// FirstInboxMessage is the DM text sent to commenters (first-inbox feature).
// Defaults to Vietnamese if empty. Operators should set this to match their page language.
FirstInboxMessage string `json:"first_inbox_message,omitempty"`
@@ -41,7 +41,7 @@ type WebhookPayload struct {
// WebhookEntry is one page's events within a webhook delivery.
type WebhookEntry struct {
ID string `json:"id"` // page_id
ID string `json:"id"` // page_id
Time int64 `json:"time"`
Changes []WebhookChange `json:"changes,omitempty"` // feed events (comments, posts)
Messaging []MessagingEvent `json:"messaging,omitempty"` // Messenger events
@@ -56,12 +56,12 @@ type WebhookChange struct {
// ChangeValue holds the details of a feed change event.
type ChangeValue struct {
From FBUser `json:"from"`
Item string `json:"item"` // "comment", "post", "status"
Item string `json:"item"` // "comment", "post", "status"
CommentID string `json:"comment_id"`
PostID string `json:"post_id"`
ParentID string `json:"parent_id"` // parent comment ID for nested replies
Message string `json:"message"`
Verb string `json:"verb"` // "add", "edit", "remove"
Verb string `json:"verb"` // "add", "edit", "remove"
CreatedTime int64 `json:"created_time"`
}
@@ -134,7 +134,7 @@ type GraphPaging struct {
// GraphListResponse is a generic Graph API list response.
type GraphListResponse[T any] struct {
Data []T `json:"data"`
Paging GraphPaging `json:"paging,omitempty"`
Paging GraphPaging `json:"paging"`
}
// graphErrorBody is the error envelope returned by Graph API on failures.
@@ -180,14 +180,6 @@ func TestShouldUseCard(t *testing.T) {
}
}
// min is a local helper for Go < 1.21 compatibility in test labels.
func min(a, b int) int {
if a < b {
return a
}
return b
}
// --- isDuplicate ---
func TestIsDuplicate_FirstTimeFalse(t *testing.T) {
@@ -221,7 +221,7 @@ func newLifecycleTestChannel(t *testing.T) *Channel {
AppID: "test-app-id",
AppSecret: "test-app-secret",
}
ch, err := New(cfg, nil, nil, nil)
ch, err := New(cfg, nil, nil, nil, nil)
if err != nil {
t.Fatalf("newTestChannel: New() error: %v", err)
}
+2 -6
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"html"
"log/slog"
"slices"
"strings"
"github.com/nextlevelbuilder/goclaw/internal/channels"
@@ -131,10 +132,5 @@ func isAssignedStaff(assigneeIDs []string, senderID string) bool {
if senderID == "" {
return false
}
for _, assigneeID := range assigneeIDs {
if assigneeID == senderID {
return true
}
}
return false
return slices.Contains(assigneeIDs, senderID)
}
@@ -206,7 +206,7 @@ func TestPostFetcher_GetPost_SingleflightCoalescing(t *testing.T) {
const goroutines = 10
var wg sync.WaitGroup
wg.Add(goroutines)
for i := 0; i < goroutines; i++ {
for range goroutines {
go func() {
defer wg.Done()
pf.GetPost(context.Background(), "post-1") //nolint:errcheck
@@ -317,10 +317,3 @@ func TestHTMLTagToWaMd_PlainText(t *testing.T) {
t.Errorf("htmlTagToWaMd(plain) = %q, want unchanged", got)
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
+14 -15
View File
@@ -5,7 +5,6 @@ import (
"testing"
"go.mau.fi/whatsmeow/proto/waE2E"
"google.golang.org/protobuf/proto"
)
// --- extractTextContent ---
@@ -18,7 +17,7 @@ func TestExtractTextContent_Nil(t *testing.T) {
}
func TestExtractTextContent_Conversation(t *testing.T) {
msg := &waE2E.Message{Conversation: proto.String("hello world")}
msg := &waE2E.Message{Conversation: new("hello world")}
got := extractTextContent(msg)
if got != "hello world" {
t.Errorf("extractTextContent(Conversation) = %q, want %q", got, "hello world")
@@ -28,7 +27,7 @@ func TestExtractTextContent_Conversation(t *testing.T) {
func TestExtractTextContent_ExtendedText(t *testing.T) {
msg := &waE2E.Message{
ExtendedTextMessage: &waE2E.ExtendedTextMessage{
Text: proto.String("extended message"),
Text: new("extended message"),
},
}
got := extractTextContent(msg)
@@ -40,10 +39,10 @@ func TestExtractTextContent_ExtendedText(t *testing.T) {
func TestExtractTextContent_ExtendedTextWithQuote(t *testing.T) {
msg := &waE2E.Message{
ExtendedTextMessage: &waE2E.ExtendedTextMessage{
Text: proto.String("my reply"),
Text: new("my reply"),
ContextInfo: &waE2E.ContextInfo{
QuotedMessage: &waE2E.Message{
Conversation: proto.String("original message"),
Conversation: new("original message"),
},
},
},
@@ -60,10 +59,10 @@ func TestExtractTextContent_ExtendedTextWithQuote(t *testing.T) {
func TestExtractTextContent_QuoteOnlyNoText(t *testing.T) {
msg := &waE2E.Message{
ExtendedTextMessage: &waE2E.ExtendedTextMessage{
Text: proto.String(""),
Text: new(""),
ContextInfo: &waE2E.ContextInfo{
QuotedMessage: &waE2E.Message{
Conversation: proto.String("quoted"),
Conversation: new("quoted"),
},
},
},
@@ -77,7 +76,7 @@ func TestExtractTextContent_QuoteOnlyNoText(t *testing.T) {
func TestExtractTextContent_ImageCaption(t *testing.T) {
msg := &waE2E.Message{
ImageMessage: &waE2E.ImageMessage{
Caption: proto.String("look at this photo"),
Caption: new("look at this photo"),
},
}
got := extractTextContent(msg)
@@ -89,7 +88,7 @@ func TestExtractTextContent_ImageCaption(t *testing.T) {
func TestExtractTextContent_VideoCaption(t *testing.T) {
msg := &waE2E.Message{
VideoMessage: &waE2E.VideoMessage{
Caption: proto.String("cool video"),
Caption: new("cool video"),
},
}
got := extractTextContent(msg)
@@ -101,7 +100,7 @@ func TestExtractTextContent_VideoCaption(t *testing.T) {
func TestExtractTextContent_DocumentCaption(t *testing.T) {
msg := &waE2E.Message{
DocumentMessage: &waE2E.DocumentMessage{
Caption: proto.String("see this document"),
Caption: new("see this document"),
},
}
got := extractTextContent(msg)
@@ -128,7 +127,7 @@ func TestExtractQuotedText_Nil(t *testing.T) {
}
func TestExtractQuotedText_Conversation(t *testing.T) {
msg := &waE2E.Message{Conversation: proto.String("quoted text")}
msg := &waE2E.Message{Conversation: new("quoted text")}
got := extractQuotedText(msg)
if got != "quoted text" {
t.Errorf("extractQuotedText(Conversation) = %q, want %q", got, "quoted text")
@@ -138,7 +137,7 @@ func TestExtractQuotedText_Conversation(t *testing.T) {
func TestExtractQuotedText_ExtendedText(t *testing.T) {
msg := &waE2E.Message{
ExtendedTextMessage: &waE2E.ExtendedTextMessage{
Text: proto.String("extended quoted"),
Text: new("extended quoted"),
},
}
got := extractQuotedText(msg)
@@ -149,7 +148,7 @@ func TestExtractQuotedText_ExtendedText(t *testing.T) {
func TestExtractQuotedText_ImageCaption(t *testing.T) {
msg := &waE2E.Message{
ImageMessage: &waE2E.ImageMessage{Caption: proto.String("img caption")},
ImageMessage: &waE2E.ImageMessage{Caption: new("img caption")},
}
got := extractQuotedText(msg)
if got != "img caption" {
@@ -159,7 +158,7 @@ func TestExtractQuotedText_ImageCaption(t *testing.T) {
func TestExtractQuotedText_VideoCaption(t *testing.T) {
msg := &waE2E.Message{
VideoMessage: &waE2E.VideoMessage{Caption: proto.String("vid caption")},
VideoMessage: &waE2E.VideoMessage{Caption: new("vid caption")},
}
got := extractQuotedText(msg)
if got != "vid caption" {
@@ -170,7 +169,7 @@ func TestExtractQuotedText_VideoCaption(t *testing.T) {
func TestExtractQuotedText_EmptyCaption(t *testing.T) {
// Image with empty caption → falls through to empty.
msg := &waE2E.Message{
ImageMessage: &waE2E.ImageMessage{Caption: proto.String("")},
ImageMessage: &waE2E.ImageMessage{Caption: new("")},
}
got := extractQuotedText(msg)
if got != "" {
+6 -7
View File
@@ -6,7 +6,6 @@ import (
"go.mau.fi/whatsmeow/proto/waE2E"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
"google.golang.org/protobuf/proto"
)
func TestIsMentioned(t *testing.T) {
@@ -15,7 +14,7 @@ func TestIsMentioned(t *testing.T) {
return &events.Message{
Message: &waE2E.Message{
ExtendedTextMessage: &waE2E.ExtendedTextMessage{
Text: proto.String("hello @bot"),
Text: new("hello @bot"),
ContextInfo: &waE2E.ContextInfo{
MentionedJID: mentionedJIDs,
},
@@ -25,11 +24,11 @@ func TestIsMentioned(t *testing.T) {
}
tests := []struct {
name string
myJID string // bot's phone JID
myLID string // bot's LID
name string
myJID string // bot's phone JID
myLID string // bot's LID
mentions []string
want bool
want bool
}{
{
name: "mentioned by phone JID",
@@ -115,7 +114,7 @@ func TestIsMentioned(t *testing.T) {
// Plain conversation message — no extended text.
evt = &events.Message{
Message: &waE2E.Message{
Conversation: proto.String("hello"),
Conversation: new("hello"),
},
}
} else {
+12 -13
View File
@@ -11,7 +11,6 @@ import (
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/proto/waE2E"
"go.mau.fi/whatsmeow/types"
"google.golang.org/protobuf/proto"
"github.com/nextlevelbuilder/goclaw/internal/bus"
)
@@ -61,7 +60,7 @@ func (c *Channel) Send(_ context.Context, msg bus.OutboundMessage) error {
chunks := chunkText(formatted, maxMessageLen)
for _, chunk := range chunks {
waMsg := &waE2E.Message{
Conversation: proto.String(chunk),
Conversation: new(chunk),
}
if _, err := c.client.SendMessage(c.ctx, chatJID, waMsg); err != nil {
return fmt.Errorf("send whatsapp message: %w", err)
@@ -90,14 +89,14 @@ func (c *Channel) buildMediaMessage(data []byte, mime, caption string) (*waE2E.M
}
return &waE2E.Message{
ImageMessage: &waE2E.ImageMessage{
Caption: proto.String(caption),
Mimetype: proto.String(mime),
Caption: new(caption),
Mimetype: new(mime),
URL: &uploaded.URL,
DirectPath: &uploaded.DirectPath,
MediaKey: uploaded.MediaKey,
FileEncSHA256: uploaded.FileEncSHA256,
FileSHA256: uploaded.FileSHA256,
FileLength: proto.Uint64(uint64(len(data))),
FileLength: new(uint64(len(data))),
},
}, nil
@@ -108,14 +107,14 @@ func (c *Channel) buildMediaMessage(data []byte, mime, caption string) (*waE2E.M
}
return &waE2E.Message{
VideoMessage: &waE2E.VideoMessage{
Caption: proto.String(caption),
Mimetype: proto.String(mime),
Caption: new(caption),
Mimetype: new(mime),
URL: &uploaded.URL,
DirectPath: &uploaded.DirectPath,
MediaKey: uploaded.MediaKey,
FileEncSHA256: uploaded.FileEncSHA256,
FileSHA256: uploaded.FileSHA256,
FileLength: proto.Uint64(uint64(len(data))),
FileLength: new(uint64(len(data))),
},
}, nil
@@ -126,13 +125,13 @@ func (c *Channel) buildMediaMessage(data []byte, mime, caption string) (*waE2E.M
}
return &waE2E.Message{
AudioMessage: &waE2E.AudioMessage{
Mimetype: proto.String(mime),
Mimetype: new(mime),
URL: &uploaded.URL,
DirectPath: &uploaded.DirectPath,
MediaKey: uploaded.MediaKey,
FileEncSHA256: uploaded.FileEncSHA256,
FileSHA256: uploaded.FileSHA256,
FileLength: proto.Uint64(uint64(len(data))),
FileLength: new(uint64(len(data))),
},
}, nil
@@ -143,14 +142,14 @@ func (c *Channel) buildMediaMessage(data []byte, mime, caption string) (*waE2E.M
}
return &waE2E.Message{
DocumentMessage: &waE2E.DocumentMessage{
Caption: proto.String(caption),
Mimetype: proto.String(mime),
Caption: new(caption),
Mimetype: new(mime),
URL: &uploaded.URL,
DirectPath: &uploaded.DirectPath,
MediaKey: uploaded.MediaKey,
FileEncSHA256: uploaded.FileEncSHA256,
FileSHA256: uploaded.FileSHA256,
FileLength: proto.Uint64(uint64(len(data))),
FileLength: new(uint64(len(data))),
},
}, nil
}
+1 -2
View File
@@ -7,7 +7,6 @@ import (
"go.mau.fi/whatsmeow/proto/waE2E"
"go.mau.fi/whatsmeow/types"
"google.golang.org/protobuf/proto"
"github.com/nextlevelbuilder/goclaw/internal/channels"
)
@@ -82,7 +81,7 @@ func (c *Channel) sendPairingReply(ctx context.Context, senderID, chatID string)
}
waMsg := &waE2E.Message{
Conversation: proto.String(replyText),
Conversation: new(replyText),
}
if _, sendErr := c.client.SendMessage(c.ctx, chatJID, waMsg); sendErr != nil {
slog.Warn("failed to send whatsapp pairing reply", "error", sendErr)
+1 -1
View File
@@ -85,7 +85,7 @@ func (m *QRMethods) runQRSession(ctx context.Context, entry *cancelEntry,
// Wait for channel to appear in manager — instance creation triggers an async
// reload, so the channel may not be registered yet when the wizard fires QR start.
var wa *Channel
for attempt := 0; attempt < 10; attempt++ {
for range 10 {
if ch, ok := m.manager.GetChannel(channelName); ok {
if w, ok := ch.(*Channel); ok {
wa = w
@@ -12,7 +12,8 @@ import (
"github.com/nextlevelbuilder/goclaw/internal/store"
)
func boolPtr(b bool) *bool { return &b }
//go:fix inline
func boolPtr(b bool) *bool { return new(b) }
func TestMergeDreamingConfigNilOverrideReturnsBase(t *testing.T) {
base := defaultDreamingConfig()
@@ -44,7 +45,7 @@ func TestMergeDreamingConfigPartialOverride(t *testing.T) {
func TestMergeDreamingConfigDisable(t *testing.T) {
base := defaultDreamingConfig()
override := &config.DreamingConfig{Enabled: boolPtr(false)}
override := &config.DreamingConfig{Enabled: new(false)}
got := mergeDreamingConfig(base, override)
if got.Enabled {
t.Errorf("Enabled = true, want false (override)")
@@ -67,7 +68,7 @@ func TestMergeDreamingConfigVerboseLogNilPreservesDefault(t *testing.T) {
func TestMergeDreamingConfigVerboseLogExplicitFalse(t *testing.T) {
base := defaultDreamingConfig()
base.VerboseLog = true
override := &config.DreamingConfig{VerboseLog: boolPtr(false)}
override := &config.DreamingConfig{VerboseLog: new(false)}
got := mergeDreamingConfig(base, override)
if got.VerboseLog {
t.Errorf("VerboseLog = true, want false (explicit override must apply)")
@@ -145,7 +146,7 @@ func TestDreamingWorkerHandleDisabledSkips(t *testing.T) {
threshold: 5,
debounce: 1 * time.Second,
resolveConfig: func(_ context.Context, _ string) *config.DreamingConfig {
return &config.DreamingConfig{Enabled: boolPtr(false)}
return &config.DreamingConfig{Enabled: new(false)}
},
}
+1 -1
View File
@@ -346,7 +346,7 @@ func TestEditionConcurrentSafety(t *testing.T) {
// Goroutine 1: repeatedly read
go func() {
for i := 0; i < 100; i++ {
for range 100 {
_ = Current()
}
done <- true
+3 -4
View File
@@ -156,10 +156,9 @@ func (m *AgentsMethods) handleUpdate(ctx context.Context, client *gateway.Client
updates["skill_evolve"] = *params.SkillEvolve
}
if params.SkillNudgeInterval != nil {
v := *params.SkillNudgeInterval
if v <= 0 {
v = 0 // DB column is NOT NULL DEFAULT 0
}
v := max(*params.SkillNudgeInterval,
// DB column is NOT NULL DEFAULT 0
0)
updates["skill_nudge_interval"] = v
}
if len(params.ReasoningConfig) > 0 {
+3 -3
View File
@@ -10,7 +10,7 @@ func TestRateLimiter_DisabledWhenZeroRPM(t *testing.T) {
if rl.Enabled() {
t.Fatal("expected disabled when rpm=0")
}
for i := 0; i < 100; i++ {
for range 100 {
if !rl.Allow("key") {
t.Fatal("expected allow when disabled")
}
@@ -43,7 +43,7 @@ func TestRateLimiter_BurstAllowed(t *testing.T) {
rl := NewRateLimiter(1, burst)
key := "test-burst"
allowed := 0
for i := 0; i < burst; i++ {
for range burst {
if rl.Allow(key) {
allowed++
}
@@ -97,7 +97,7 @@ func TestRateLimiter_DefaultBurstWhenZero(t *testing.T) {
key := "burst-default"
// Should be able to consume at least 5 tokens from the default burst.
allowed := 0
for i := 0; i < 5; i++ {
for range 5 {
if rl.Allow(key) {
allowed++
}
+36 -35
View File
@@ -76,10 +76,10 @@ func TestDeref(t *testing.T) {
expect string
}{
{"nil_pointer", nil, ""},
{"empty_string", strPtr(""), ""},
{"normal_string", strPtr("hello"), "hello"},
{"whitespace", strPtr(" "), " "},
{"unicode_string", strPtr("你好"), "你好"},
{"empty_string", new(""), ""},
{"normal_string", new("hello"), "hello"},
{"whitespace", new(" "), " "},
{"unicode_string", new("你好"), "你好"},
}
for _, tt := range tests {
@@ -94,17 +94,17 @@ func TestDeref(t *testing.T) {
func TestProcessResponse(t *testing.T) {
tests := []struct {
name string
response string
maxChars int
name string
response string
maxChars int
expectDeliver bool
description string
description string
}{
{
name: "empty_response",
response: "",
name: "empty_response",
response: "",
expectDeliver: true,
description: "empty response should deliver (no HEARTBEAT_OK token)",
description: "empty response should deliver (no HEARTBEAT_OK token)",
},
{
name: "heartbeat_ok_only",
@@ -119,16 +119,16 @@ func TestProcessResponse(t *testing.T) {
description: "HEARTBEAT_OK anywhere in response suppresses delivery",
},
{
name: "normal_content",
response: "System check: CPU 45%, Memory 60%, Disk 70%",
name: "normal_content",
response: "System check: CPU 45%, Memory 60%, Disk 70%",
expectDeliver: true,
description: "normal content without HEARTBEAT_OK is delivered",
description: "normal content without HEARTBEAT_OK is delivered",
},
{
name: "ok_token_case_sensitive",
response: "heartbeat_ok Status is good",
name: "ok_token_case_sensitive",
response: "heartbeat_ok Status is good",
expectDeliver: true,
description: "HEARTBEAT_OK is case-sensitive, lowercase should not suppress",
description: "HEARTBEAT_OK is case-sensitive, lowercase should not suppress",
},
{
name: "heartbeat_ok_at_start",
@@ -143,10 +143,10 @@ func TestProcessResponse(t *testing.T) {
description: "HEARTBEAT_OK at end suppresses",
},
{
name: "multiline_without_token",
response: "Line 1\nLine 2\nLine 3",
name: "multiline_without_token",
response: "Line 1\nLine 2\nLine 3",
expectDeliver: true,
description: "multiline without HEARTBEAT_OK is delivered",
description: "multiline without HEARTBEAT_OK is delivered",
},
{
name: "multiline_with_token",
@@ -155,10 +155,10 @@ func TestProcessResponse(t *testing.T) {
description: "HEARTBEAT_OK in middle of multiline suppresses",
},
{
name: "long_response",
response: "This is a very long response with lots of details about the system status, but no special tokens",
name: "long_response",
response: "This is a very long response with lots of details about the system status, but no special tokens",
expectDeliver: true,
description: "long response without token is delivered",
description: "long response without token is delivered",
},
}
@@ -173,8 +173,9 @@ func TestProcessResponse(t *testing.T) {
}
}
//go:fix inline
func strPtr(s string) *string {
return &s
return new(s)
}
func TestIsWithinActiveHours_NoConfig(t *testing.T) {
@@ -192,8 +193,8 @@ func TestIsWithinActiveHours_NoConfig(t *testing.T) {
func TestIsWithinActiveHours_EmptyConfig(t *testing.T) {
// When active hours are empty strings, should always return true
hb := &store.AgentHeartbeat{
ActiveHoursStart: strPtr(""),
ActiveHoursEnd: strPtr(""),
ActiveHoursStart: new(""),
ActiveHoursEnd: new(""),
}
got := isWithinActiveHours(*hb)
if !got {
@@ -219,11 +220,11 @@ func TestIsWithinActiveHours_SimpleWindow(t *testing.T) {
}
tests := []struct {
name string
hour int
minute int
startStr string
endStr string
name string
hour int
minute int
startStr string
endStr string
expectInside bool
}{
// Within window
@@ -270,9 +271,9 @@ func TestIsWithinActiveHours_MidnightWrap(t *testing.T) {
}
tests := []struct {
name string
hour int
minute int
name string
hour int
minute int
expectInside bool
}{
{"midnight_10pm", 22, 0, true}, // 22:00 is in window
@@ -302,5 +303,5 @@ func formatInt(i int) string {
if i < 10 {
return "0" + string(rune('0'+i))
}
return string(rune('0' + i/10)) + string(rune('0'+i%10))
return string(rune('0'+i/10)) + string(rune('0'+i%10))
}
@@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"io"
"maps"
"net/http"
"net/http/httptest"
"strings"
@@ -130,9 +131,7 @@ func (s *stubTenantCfgStore) ListAll(_ context.Context, tid uuid.UUID) (map[stri
s.mu.Lock()
defer s.mu.Unlock()
out := make(map[string]bool, len(s.enabled))
for k, v := range s.enabled {
out[k] = v
}
maps.Copy(out, s.enabled)
return out, nil
}
+2 -3
View File
@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"log/slog"
"maps"
"net/http"
"os"
"path/filepath"
@@ -232,9 +233,7 @@ func (h *SkillsHandler) handleUpload(w http.ResponseWriter, r *http.Request) {
)
skill.Status = depState.status
skill.MissingDeps = depState.missing
for key, value := range depState.response {
response[key] = value
}
maps.Copy(response, depState.response)
}
}
+8 -9
View File
@@ -5,6 +5,7 @@ import (
"bytes"
"context"
"encoding/json"
"maps"
"mime/multipart"
"net/http"
"net/http/httptest"
@@ -365,12 +366,12 @@ func skillMarkdown(name, slug string) string {
}
type skillManageStoreStub struct {
baseDir string
version int64
nextBySlug map[string]int
skills map[uuid.UUID]store.SkillInfo
systemDirs map[string]string
hashBySlug map[string]string // slug -> SKILL.md content hash (most recent)
baseDir string
version int64
nextBySlug map[string]int
skills map[uuid.UUID]store.SkillInfo
systemDirs map[string]string
hashBySlug map[string]string // slug -> SKILL.md content hash (most recent)
}
func newSkillManageStoreStub(baseDir string) *skillManageStoreStub {
@@ -515,9 +516,7 @@ func (s *skillManageStoreStub) ListAllSystemSkills(context.Context) []store.Skil
}
func (s *skillManageStoreStub) ListSystemSkillDirs(context.Context) map[string]string {
out := make(map[string]string, len(s.systemDirs))
for slug, dir := range s.systemDirs {
out[slug] = dir
}
maps.Copy(out, s.systemDirs)
return out
}
func (s *skillManageStoreStub) StoreMissingDeps(_ context.Context, id uuid.UUID, missing []string) error {
@@ -125,7 +125,7 @@ func TestSplitChunks_Empty(t *testing.T) {
func TestSplitChunks_MultipleChunks(t *testing.T) {
// 5 paragraphs of 40 chars each; max=50 → multiple chunks.
var paras []string
for i := 0; i < 5; i++ {
for i := range 5 {
paras = append(paras, strings.Repeat(fmt.Sprintf("%d", i), 40))
}
text := strings.Join(paras, "\n\n")
@@ -341,7 +341,7 @@ func TestExtract_LongText_SplitsIntoChunks(t *testing.T) {
// Build text longer than maxChunkChars (12000) with paragraph breaks.
para := strings.Repeat("word ", 200) // ~1000 chars per para
var paras []string
for i := 0; i < 15; i++ {
for range 15 {
paras = append(paras, para)
}
longText := strings.Join(paras, "\n\n") // ~15000+ chars
+3 -5
View File
@@ -209,12 +209,10 @@ func TestLimitedWriter_ZeroMax(t *testing.T) {
func TestLimitedWriter_ConcurrentWrites(t *testing.T) {
lw := &limitedWriter{max: 1000}
var wg sync.WaitGroup
for i := 0; i < 20; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for range 20 {
wg.Go(func() {
lw.Write([]byte("data"))
}()
})
}
wg.Wait()
// No panic and output <= max
+1 -8
View File
@@ -470,13 +470,6 @@ func TestConn_ReadLoop_MalformedInputNoPanic(t *testing.T) {
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func TestConn_IDIncrement(t *testing.T) {
// Verify each Call gets a unique ID by capturing two requests
clientR, serverW := io.Pipe()
@@ -489,7 +482,7 @@ func TestConn_IDIncrement(t *testing.T) {
// Goroutine that reads requests and responds
go func() {
for i := 0; i < 2; i++ {
for range 2 {
buf := make([]byte, 4096)
n, _ := serverR.Read(buf)
var req jsonrpcMessage
+1 -1
View File
@@ -91,7 +91,7 @@ func TestProcessPool_Close_WithFakeProcesses(t *testing.T) {
pp := NewProcessPool("x", nil, "", time.Minute)
// Insert fake ACPProcess entries that are already exited
for i := 0; i < 3; i++ {
for i := range 3 {
exitedCh := make(chan struct{})
close(exitedCh) // pre-exited
proc := &ACPProcess{
+4 -6
View File
@@ -76,7 +76,7 @@ func TestCappedBuffer_OverflowExceedsExistingBuf(t *testing.T) {
func TestCappedBuffer_MultipleSmallWrites(t *testing.T) {
cb := &cappedBuffer{max: 10}
for i := 0; i < 5; i++ {
for range 5 {
cb.Write([]byte("ab"))
}
got := cb.String()
@@ -101,12 +101,10 @@ func TestCappedBuffer_ZeroMax(t *testing.T) {
func TestCappedBuffer_ConcurrentWrites(t *testing.T) {
cb := &cappedBuffer{max: 100}
var wg sync.WaitGroup
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for range 50 {
wg.Go(func() {
cb.Write([]byte("data"))
}()
})
}
wg.Wait()
s := cb.String()
+2 -2
View File
@@ -76,7 +76,7 @@ func TestAdapterRegistry_ConcurrentRegister(t *testing.T) {
})
var wg sync.WaitGroup
for i := 0; i < 32; i++ {
for i := range 32 {
wg.Add(2)
go func(id int) {
defer wg.Done()
@@ -144,5 +144,5 @@ func (s *stubAdapter) Capabilities() ProviderCapabilities { return ProviderCapab
func (s *stubAdapter) ToRequest(ChatRequest) ([]byte, http.Header, error) {
return nil, nil, nil
}
func (s *stubAdapter) FromResponse([]byte) (*ChatResponse, error) { return nil, nil }
func (s *stubAdapter) FromResponse([]byte) (*ChatResponse, error) { return nil, nil }
func (s *stubAdapter) FromStreamChunk([]byte) (*StreamChunk, error) { return nil, nil }
+4 -4
View File
@@ -15,14 +15,14 @@ const CacheBoundaryMarker = "<!-- GOCLAW_CACHE_BOUNDARY -->"
// Returns 1 block with cache_control if no boundary (backwards compat).
func splitSystemPromptForCache(content string) []map[string]any {
ephemeral := map[string]any{"type": "ephemeral"}
idx := strings.Index(content, CacheBoundaryMarker)
if idx == -1 {
before, after, ok := strings.Cut(content, CacheBoundaryMarker)
if !ok {
return []map[string]any{
{"type": "text", "text": content, "cache_control": ephemeral},
}
}
stable := strings.TrimSpace(content[:idx])
dynamic := strings.TrimSpace(content[idx+len(CacheBoundaryMarker):])
stable := strings.TrimSpace(before)
dynamic := strings.TrimSpace(after)
blocks := []map[string]any{
{"type": "text", "text": stable, "cache_control": ephemeral},
}
+4 -3
View File
@@ -266,11 +266,12 @@ func sanitizePathSegment(s string) string {
// Payload: agentID|userID|channel|chatID|peerKind|workspace|tenantID
func SignBridgeContext(key, agentID, userID, channel, chatID, peerKind, workspace, tenantID string, extra ...string) string {
mac := hmac.New(sha256.New, []byte(key))
payload := agentID + "|" + userID + "|" + channel + "|" + chatID + "|" + peerKind + "|" + workspace + "|" + tenantID
var payload strings.Builder
payload.WriteString(agentID + "|" + userID + "|" + channel + "|" + chatID + "|" + peerKind + "|" + workspace + "|" + tenantID)
for _, e := range extra {
payload += "|" + e
payload.WriteString("|" + e)
}
mac.Write([]byte(payload))
mac.Write([]byte(payload.String()))
return hex.EncodeToString(mac.Sum(nil))
}
+1 -1
View File
@@ -92,7 +92,7 @@ func TestContactCollector_SameTenantDedup(t *testing.T) {
tenant := uuid.New()
ctx := WithTenantID(context.Background(), tenant)
for i := 0; i < 5; i++ {
for range 5 {
c.EnsureContact(ctx, "telegram", "tg-main", "user-123", "uid-1", "Alice", "alice", "user", "user", "", "")
}
+1 -4
View File
@@ -48,10 +48,7 @@ func (s *PGTeamStore) BatchGetTaskSiblingsByBasenames(
const chunkSize = 500
for start := 0; start < len(clean); start += chunkSize {
end := start + chunkSize
if end > len(clean) {
end = len(clean)
}
end := min(start+chunkSize, len(clean))
chunk := clean[start:end]
const q = `
+3 -5
View File
@@ -470,13 +470,11 @@ func TestTaskTicker_ConcurrentStartStop_Race(t *testing.T) {
tt.Start()
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for range 5 {
wg.Go(func() {
// Access cooldown map concurrently via pruneCooldowns (uses lock).
tt.pruneCooldowns()
}()
})
}
wg.Wait()
tt.Stop()
+1 -1
View File
@@ -67,7 +67,7 @@ func BenchmarkCountMessages(b *testing.B) {
func BenchmarkCountMessages_LargeHistory(b *testing.B) {
tc := NewTiktokenCounter()
msgs := make([]providers.Message, 50)
for i := 0; i < 50; i++ {
for i := range 50 {
role := "user"
if i%2 == 1 {
role = "assistant"
+3 -6
View File
@@ -3,6 +3,7 @@ package tools
import (
"context"
"log/slog"
"maps"
"sync"
"github.com/google/uuid"
@@ -255,12 +256,8 @@ func BuiltinToolSettingsFromCtx(ctx context.Context) BuiltinToolSettings {
// Both tiers present: layer tenant override on top of global defaults.
merged := make(BuiltinToolSettings, len(global)+len(tenant))
for k, v := range global {
merged[k] = v
}
for k, v := range tenant {
merged[k] = v
}
maps.Copy(merged, global)
maps.Copy(merged, tenant)
return merged
}
+3 -4
View File
@@ -11,6 +11,7 @@ import (
"os"
"os/exec"
"regexp"
"slices"
"strings"
"time"
@@ -189,10 +190,8 @@ func matchesBinaryVerbose(args []string, denyPatternsJSON json.RawMessage) strin
slog.Warn("secure_cli.invalid_deny_pattern", "pattern", p, "error", err)
continue
}
for _, arg := range args {
if re.MatchString(arg) {
return p
}
if slices.ContainsFunc(args, re.MatchString) {
return p
}
}
return ""
+10 -10
View File
@@ -32,7 +32,7 @@ func (m *MockTool) Execute(ctx context.Context, args map[string]any) *Result {
// BenchmarkRegistry_Get_50Tools benchmarks tool lookup in registry with 50 tools.
func BenchmarkRegistry_Get_50Tools(b *testing.B) {
reg := NewRegistry()
for i := 0; i < 50; i++ {
for i := range 50 {
tool := &MockTool{
name: fmt.Sprintf("tool_%02d", i),
description: "Mock tool for benchmarking",
@@ -58,7 +58,7 @@ func BenchmarkRegistry_Get_50Tools(b *testing.B) {
// BenchmarkRegistry_Get_100Tools benchmarks tool lookup in registry with 100 tools.
func BenchmarkRegistry_Get_100Tools(b *testing.B) {
reg := NewRegistry()
for i := 0; i < 100; i++ {
for i := range 100 {
tool := &MockTool{
name: fmt.Sprintf("tool_%03d", i),
description: "Mock tool for benchmarking",
@@ -84,7 +84,7 @@ func BenchmarkRegistry_Get_100Tools(b *testing.B) {
// BenchmarkRegistry_List_50Tools benchmarks listing tools from registry with 50 tools.
func BenchmarkRegistry_List_50Tools(b *testing.B) {
reg := NewRegistry()
for i := 0; i < 50; i++ {
for i := range 50 {
tool := &MockTool{
name: fmt.Sprintf("tool_%02d", i),
description: "Mock tool for benchmarking",
@@ -102,7 +102,7 @@ func BenchmarkRegistry_List_50Tools(b *testing.B) {
// BenchmarkRegistry_List_100Tools benchmarks listing tools from registry with 100 tools.
func BenchmarkRegistry_List_100Tools(b *testing.B) {
reg := NewRegistry()
for i := 0; i < 100; i++ {
for i := range 100 {
tool := &MockTool{
name: fmt.Sprintf("tool_%03d", i),
description: "Mock tool for benchmarking",
@@ -120,7 +120,7 @@ func BenchmarkRegistry_List_100Tools(b *testing.B) {
// BenchmarkRegistry_ProviderDefs_50Tools benchmarks generating provider definitions for 50 tools.
func BenchmarkRegistry_ProviderDefs_50Tools(b *testing.B) {
reg := NewRegistry()
for i := 0; i < 50; i++ {
for i := range 50 {
tool := &MockTool{
name: fmt.Sprintf("tool_%02d", i),
description: "Mock tool for benchmarking",
@@ -144,7 +144,7 @@ func BenchmarkRegistry_ProviderDefs_50Tools(b *testing.B) {
// BenchmarkRegistry_Alias_50Tools benchmarks tool lookup with aliases in registry.
func BenchmarkRegistry_Alias_50Tools(b *testing.B) {
reg := NewRegistry()
for i := 0; i < 50; i++ {
for i := range 50 {
tool := &MockTool{
name: fmt.Sprintf("tool_%02d", i),
description: "Mock tool for benchmarking",
@@ -165,7 +165,7 @@ func BenchmarkRegistry_Alias_50Tools(b *testing.B) {
// BenchmarkRegistry_Disable_Enable benchmarks disabling and enabling tools.
func BenchmarkRegistry_Disable_Enable(b *testing.B) {
reg := NewRegistry()
for i := 0; i < 50; i++ {
for i := range 50 {
tool := &MockTool{
name: fmt.Sprintf("tool_%02d", i),
description: "Mock tool for benchmarking",
@@ -185,7 +185,7 @@ func BenchmarkRegistry_Disable_Enable(b *testing.B) {
// BenchmarkRegistry_Count benchmarks counting tools in registry.
func BenchmarkRegistry_Count(b *testing.B) {
reg := NewRegistry()
for i := 0; i < 50; i++ {
for i := range 50 {
tool := &MockTool{
name: fmt.Sprintf("tool_%02d", i),
description: "Mock tool for benchmarking",
@@ -203,7 +203,7 @@ func BenchmarkRegistry_Count(b *testing.B) {
// BenchmarkRegistry_Resolve_WithDisabled benchmarks resolving tools with some disabled.
func BenchmarkRegistry_Resolve_WithDisabled(b *testing.B) {
reg := NewRegistry()
for i := 0; i < 50; i++ {
for i := range 50 {
tool := &MockTool{
name: fmt.Sprintf("tool_%02d", i),
description: "Mock tool for benchmarking",
@@ -212,7 +212,7 @@ func BenchmarkRegistry_Resolve_WithDisabled(b *testing.B) {
}
// Disable half the tools
for i := 0; i < 25; i++ {
for i := range 25 {
reg.Disable(fmt.Sprintf("tool_%02d", i))
}
+1 -1
View File
@@ -80,7 +80,7 @@ func findOrphanSleeps(t *testing.T) []string {
}
var found []string
for _, line := range strings.Split(string(out), "\n") {
for line := range strings.SplitSeq(string(out), "\n") {
// Match lines containing "sleep 60" but not the grep/ps command itself.
if strings.Contains(line, "sleep 60") && !strings.Contains(line, "ps aux") {
fields := strings.Fields(line)
+2 -6
View File
@@ -1,6 +1,7 @@
package tools
import (
"slices"
"strings"
"time"
@@ -98,12 +99,7 @@ func NormalizeWebSearchProviderOrder(order []string) []string {
}
func isKnownSearchProvider(id string) bool {
for _, p := range defaultSearchProviderOrder {
if p == id {
return true
}
}
return false
return slices.Contains(defaultSearchProviderOrder, id)
}
// --- Shared provider helpers ---
+1 -1
View File
@@ -219,7 +219,7 @@ func TestShutdown_WorkerExitsCleanly(t *testing.T) {
c := newTestCollector(&alwaysFailStore{})
// Put a few items in the retry channel before starting worker.
for i := 0; i < 5; i++ {
for range 5 {
c.retryCh <- pendingUpdate{TraceID: uuid.New(), Updates: map[string]any{"status": "pending"}}
}
+2 -4
View File
@@ -29,12 +29,10 @@ func CalculateCost(pricing *config.ModelPricing, usage *providers.Usage) float64
// ReasoningPerMillion rate is set. Otherwise price the full CompletionTokens
// at OutputPerMillion — matches the provider billing semantics described above.
if pricing.ReasoningPerMillion > 0 && usage.ThinkingTokens > 0 {
visible := usage.CompletionTokens - usage.ThinkingTokens
if visible < 0 {
visible := max(usage.CompletionTokens-usage.ThinkingTokens,
// Defensive: thinkingChars/4 estimate for Anthropic may exceed OutputTokens
// under unusual streaming conditions. Clamp to zero instead of going negative.
visible = 0
}
0)
cost += float64(visible) * pricing.OutputPerMillion / 1_000_000
cost += float64(usage.ThinkingTokens) * pricing.ReasoningPerMillion / 1_000_000
} else {
+30 -29
View File
@@ -16,61 +16,61 @@ func TestInferOwnerFromPath(t *testing.T) {
}
tests := []struct {
path string
wantAgentID *string
wantTeamID *string
wantScope string
path string
wantAgentID *string
wantTeamID *string
wantScope string
wantStrippedPath string
}{
// Legacy agents/{key}/... → personal scope, full path preserved
{
path: "agents/my-bot/notes/todo.md",
wantAgentID: strPtr("uuid-1"),
wantScope: "personal",
path: "agents/my-bot/notes/todo.md",
wantAgentID: new("uuid-1"),
wantScope: "personal",
wantStrippedPath: "agents/my-bot/notes/todo.md",
},
{
path: "agents/my-bot/file.md",
wantAgentID: strPtr("uuid-1"),
wantScope: "personal",
path: "agents/my-bot/file.md",
wantAgentID: new("uuid-1"),
wantScope: "personal",
wantStrippedPath: "agents/my-bot/file.md",
},
// Root-level {agent_key}/... → personal scope (workspace layout)
{
path: "my-bot/telegram/123/report.md",
wantAgentID: strPtr("uuid-1"),
wantScope: "personal",
path: "my-bot/telegram/123/report.md",
wantAgentID: new("uuid-1"),
wantScope: "personal",
wantStrippedPath: "my-bot/telegram/123/report.md",
},
{
path: "other-bot/docs/guide.md",
wantAgentID: strPtr("uuid-2"),
wantScope: "personal",
path: "other-bot/docs/guide.md",
wantAgentID: new("uuid-2"),
wantScope: "personal",
wantStrippedPath: "other-bot/docs/guide.md",
},
// teams/{uuid}/... → team scope, full path preserved
{
path: "teams/" + validUUID + "/doc.md",
wantTeamID: strPtr(validUUID),
wantScope: "team",
path: "teams/" + validUUID + "/doc.md",
wantTeamID: new(validUUID),
wantScope: "team",
wantStrippedPath: "teams/" + validUUID + "/doc.md",
},
{
path: "teams/" + validUUID + "/deep/nested.md",
wantTeamID: strPtr(validUUID),
wantScope: "team",
path: "teams/" + validUUID + "/deep/nested.md",
wantTeamID: new(validUUID),
wantScope: "team",
wantStrippedPath: "teams/" + validUUID + "/deep/nested.md",
},
// Root-level file (no slash) → shared
{
path: "README.md",
wantScope: "shared",
path: "README.md",
wantScope: "shared",
wantStrippedPath: "README.md",
},
// Nested file not matching any agent key → shared
{
path: "docs/guide.md",
wantScope: "shared",
path: "docs/guide.md",
wantScope: "shared",
wantStrippedPath: "docs/guide.md",
},
// Unknown agent under agents/ prefix → skip
@@ -90,8 +90,8 @@ func TestInferOwnerFromPath(t *testing.T) {
},
// Unknown root folder (not an agent key) → shared
{
path: "telegram/group/file.md",
wantScope: "shared",
path: "telegram/group/file.md",
wantScope: "shared",
wantStrippedPath: "telegram/group/file.md",
},
}
@@ -203,4 +203,5 @@ func TestInferTitle(t *testing.T) {
}
}
func strPtr(s string) *string { return &s }
//go:fix inline
func strPtr(s string) *string { return new(s) }
+4 -4
View File
@@ -99,7 +99,7 @@ func TestSafeWalkWorkspace_ExcludedPaths(t *testing.T) {
func TestSafeWalkWorkspace_MaxFileLimit(t *testing.T) {
dir := t.TempDir()
for i := 0; i < 20; i++ {
for i := range 20 {
writeFile(t, dir, filepath.Join("files", string(rune('a'+i))+".txt"), "data")
}
@@ -122,7 +122,7 @@ func TestSafeWalkWorkspace_MaxTotalBytes(t *testing.T) {
// Create files that exceed total byte limit. Use a whitelisted extension
// (.txt) so the files actually register — Phase 01 whitelist would skip .bin.
bigContent := make([]byte, 1024) // 1KB each
for i := 0; i < 10; i++ {
for i := range 10 {
writeFile(t, dir, filepath.Join("data", string(rune('a'+i))+".txt"), string(bigContent))
}
@@ -143,7 +143,7 @@ func TestSafeWalkWorkspace_MaxTotalBytes(t *testing.T) {
func TestSafeWalkWorkspace_ContextCancel(t *testing.T) {
dir := t.TempDir()
for i := 0; i < 50; i++ {
for i := range 50 {
writeFile(t, dir, filepath.Join("files", string(rune('a'+i/26))+"_"+string(rune('a'+i%26))+".txt"), "data")
}
@@ -208,7 +208,7 @@ func TestIsExcludedPath(t *testing.T) {
{"images/screenshot.png", false},
{"teams/abc-123/doc.md", false},
{"soul-notes.md", false},
{"deep/SOUL.md", false}, // not root-level context file
{"deep/SOUL.md", false}, // not root-level context file
{".uploads/photo.jpg", false},
{"report.pdf", false},
}