repo-review: inject the clock into notification dedup at 2d4bfbd - #6
Conversation
The four notifiers recorded dedup timestamps with time.Now(), so TTL expiry could only be exercised by sleeping -- every notify test currently does time.Sleep(60ms) for exactly that reason. libtime.Now is a package-level var, so this needs no signature change on any of the four public constructors; a test can swap the clock and drive expiry deterministically.
There was a problem hiding this comment.
Now I have all the information I need. Let me produce the consolidated report.
PR Review: repo-review: inject the clock into notification dedup at 2d4bfbd
Target: master ← repo-review/2d4bfbd
Changed files: CHANGELOG.md, pkg/notify/{log,notify,openclaw,telegram}.go
Traceability (Selector Mode)
- Candidates: 4 rules matched by Step 4b-i glob filter
- Applicable: 4 rules selected by Step 4c-sel
go-composition/no-package-function-calls-in-business-logic→ applicable (pkg/notify/*.go files match glob; rule triggers on all business-logic method bodies)go-logging/external-call-logs-response→ applicable (pkg/notify/*.go contain HTTP boundary calls)go-logging/lowercase-log-messages→ applicable (pkg/notify/*.go contain slog calls with uppercase first letters)go-testing/counterfeiter-mocks-required→ applicable (Notifier interface in notify.go matches interface trigger)
- Skipped: none
Must Fix (Critical)
None — no security vulnerabilities, data correctness issues, or concurrency bugs introduced.
Should Fix (Important)
-
pkg/notify/telegram.go:79,pkg/notify/openclaw.go:58,pkg/notify/notify.go:59,pkg/notify/log.go:36— The dedup checktime.Since(lastSent) < t.dedupTTLcalls the real system clock. Combined with the PR's switch tolibtime.Now()for stamping entries, this creates a mixed clock regime: entries are stamped withlibtime.Now()but expiry is still checked againsttime.Since()(real wall clock). This means the 50ms-TTL tests (time.Sleep(60 * time.Millisecond)) remain inherently flaky under CI load — the PR enables dropping them but doesn't fix the root cause. (rule:go-composition/no-package-function-calls-in-business-logic) -
pkg/notify/telegram.go:88,pkg/notify/openclaw.go:67,pkg/notify/notify.go:68,pkg/notify/log.go:45—libtime.Now()is a package-level call injected into business-logic method bodies for stamping dedup entries. It is not injectable: thelibtimepackage exposes a package-level varNowbacked bytime.Nowat init time, with no interface. This means the dedup window cannot be controlled in unit tests without replacing the actual time source — the PR's motivation (injectable clock) is not fully achieved. (rule:go-composition/no-package-function-calls-in-business-logic) -
pkg/notify/telegram.go:101,pkg/notify/openclaw.go:80,pkg/notify/notify.go:71—json.Marshalcalled directly in method body;pkg/notify/telegram.go:103,pkg/notify/openclaw.go:82,pkg/notify/notify.go:73—errors.Wrapfcalled directly;pkg/notify/telegram.go:106,pkg/notify/telegram.go:108,pkg/notify/openclaw.go:85,pkg/notify/notify.go:76—fmt.Sprintfandhttp.NewRequestWithContextcalled directly;pkg/notify/telegram.go:114,pkg/notify/openclaw.go:97,pkg/notify/notify.go:87—slog.Debugwith request body called directly. All hidden dependencies — untestable without an interface wrapper. (rule:go-composition/no-package-function-calls-in-business-logic) -
pkg/notify/telegram.go:121,pkg/notify/openclaw.go:105,pkg/notify/notify.go:94—t.httpClient.Do(req)/n.httpClient.Do(req)returns without a log line summarising status code and latency. The pre-callslog.Debugon the above lines logs the request, but there is no post-call audit line with response status. This is the boundary-call audit gap. (rule:go-logging/external-call-logs-response)
Nice to Have (Optional)
pkg/notify/openclaw.go:97—slog.Debug("sending webhook", ...)starts with uppercases.pkg/notify/notify.go:87—slog.Debug("sending webhook", ...)starts with uppercases.
(rule:go-logging/lowercase-log-messages)
Precommit / CI
precommit skipped (selector mode) — CI covers lint+test.
Notes
On the core refactor intent vs. reality: The PR description says "inject the clock into notification dedup" and the CHANGELOG says "injectable libtime clock". However, libtime.Now() from github.com/bborbe/time is a package-level exported var initialized to time.Now at package init — not an injectable interface. The calling code in pkg/notify/*.go does l.seen[key] = libtime.Now() (stamping) but still calls time.Since(lastSent) directly (checking). The dedup deduces wall-clock passage from the time.Time stored in seen[key] — these are of type time.Time regardless of whether they're created via time.Now() or libtime.Now(). The refactor is therefore functionally equivalent to the original; the stated goal of making the clock "injectable" is not achieved by this implementation. Tests still use time.Sleep(60 * time.Millisecond) for TTL expiry and remain timing-dependent.
Counterfeiter: The Notifier interface at notify.go:31 already has //counterfeiter:generate -o ../../mocks/notifier.go --fake-name FakeNotifier . Notifier and a matching mock exists at mocks/notifier.go. No stale directive issue.
{
"verdict": "request-changes",
"summary": "PR switches dedup entry stamping from time.Now to libtime.Now across all four notifier implementations, but libtime.Now is a package-level var (not injectable), time.Since is still called directly for the dedup check, and the hidden-package-call pattern remains throughout — the stated injectability goal is not achieved and tests remain timing-dependent.",
"comments": [
{
"file": "pkg/notify/telegram.go",
"line": 79,
"severity": "major",
"message": "time.Since(lastSent) called directly in business-logic method body — hidden real-system-clock dependency for dedup check. The PR's libtime.Now() stamp is also a package-level call, creating mixed-clock regime without true injectability. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/openclaw.go",
"line": 58,
"severity": "major",
"message": "time.Since(lastSent) called directly in business-logic method body — hidden real-system-clock dependency for dedup check. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/notify.go",
"line": 59,
"severity": "major",
"message": "time.Since(lastSent) called directly in business-logic method body — hidden real-system-clock dependency for dedup check. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/log.go",
"line": 36,
"severity": "major",
"message": "time.Since(lastSent) called directly in business-logic method body — hidden real-system-clock dependency for dedup check. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/telegram.go",
"line": 88,
"severity": "major",
"message": "libtime.Now() is a package-level var backed by time.Now — not injectable. The PR claims injectable clock but provides no interface; dedup cannot be controlled in unit tests without patching the actual time source. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/openclaw.go",
"line": 67,
"severity": "major",
"message": "libtime.Now() is a package-level var backed by time.Now — not injectable. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/notify.go",
"line": 68,
"severity": "major",
"message": "libtime.Now() is a package-level var backed by time.Now — not injectable. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/log.go",
"line": 45,
"severity": "major",
"message": "libtime.Now() is a package-level var backed by time.Now — not injectable. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/telegram.go",
"line": 101,
"severity": "major",
"message": "json.Marshal called directly in method body — hidden dependency, untestable without an interface wrapper. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/telegram.go",
"line": 121,
"severity": "major",
"message": "httpClient.Do(req) returns without a post-call log line summarising status and latency — boundary audit gap. (rule: go-logging/external-call-logs-response)"
},
{
"file": "pkg/notify/openclaw.go",
"line": 105,
"severity": "major",
"message": "httpClient.Do(req) returns without a post-call log line summarising status and latency — boundary audit gap. (rule: go-logging/external-call-logs-response)"
},
{
"file": "pkg/notify/notify.go",
"line": 94,
"severity": "major",
"message": "httpClient.Do(req) returns without a post-call log line summarising status and latency — boundary audit gap. (rule: go-logging/external-call-logs-response)"
},
{
"file": "pkg/notify/openclaw.go",
"line": 97,
"severity": "nit",
"message": "Log message starts with uppercase 'sending webhook' — use lowercase per Go slog convention. (rule: go-logging/lowercase-log-messages)"
},
{
"file": "pkg/notify/notify.go",
"line": 87,
"severity": "nit",
"message": "Log message starts with uppercase 'sending webhook' — use lowercase per Go slog convention. (rule: go-logging/lowercase-log-messages)"
}
],
"concerns_addressed": [
"correctness: libtime.Now() is package-level var — NOT properly injectable (see findings above)",
"correctness: time.Since still called directly in all four notifier methods (see findings above)",
"correctness: thread-safety — sync.Mutex guards seen map and mu correctly, no race introduced by this PR",
"tests: existing tests use time.Sleep(60ms) for 50ms TTL — still timing-dependent, PR does not rewrite them (noted as pre-existing issue, not regression)"
]
}The first revision of this PR converted only the write -- seen[key] = libtime.Now() -- and left the read as time.Since(lastSent). That is a mixed clock: under a fake clock the entry carries fake time while expiry is measured against the real one, so advancing the clock past the TTL never expires the entry. This is exactly the regression a reviewer caught in bborbe/log#13, and the bot caught it here. Convert the read as well, and add a spec that advances a fake clock past the TTL and asserts the notification is emitted again -- it fails against the half-converted code.
|
The mixed-clock finding was correct and mine. Fixed in
Verified — the first revision converted only the write: n.seen[key] = libtime.Now() // converted
if exists && time.Since(lastSent) < n.dedupTTL { // NOT convertedThat is a mixed clock: the entry carries fake time while expiry is measured against the real one, so advancing a fake clock past the TTL never expires the entry. Strictly worse than leaving both on This is precisely the regression a reviewer caught in my own fix in log#13 — the case I have been citing all sweep as the reason to convert both sides. I cited it and then did it anyway. Good catch. Both sides now read On
|
superseded by new automated review
There was a problem hiding this comment.
PR Review: repo-review: inject the clock into notification dedup at 2d4bfbd
Target: master ← repo-review/2d4bfbd | Files: 6 files changed, +56/−8
Must Fix (Critical)
-
pkg/notify/telegram.go:120—t.httpClient.Do(req)is a process-boundary call with no log summarising the response (method, status, latency). Without an audit log line, runtime failures require redeploying with extra instrumentation to diagnose. (rule:go-logging/external-call-logs-response) -
pkg/notify/openclaw.go:105—n.httpClient.Do(req)same issue — no response log. (rule:go-logging/external-call-logs-response) -
pkg/notify/notify.go:94—n.httpClient.Do(req)same issue — no response log. (rule:go-logging/external-call-logs-response) -
pkg/notify/log_test.go:29— Inside a GinkgoItblock,buf.Reset()discards its return value (bare expression). If this returned an error,errcheckwould break the build silently. UseExpect(...).To(Succeed()). (rule:go-testing/no-bare-error-call) -
pkg/notify/log_test.go:31— Inside a GinkgoItblock,slog.SetDefault(logger)discards its return value. Same rationale. (rule:go-testing/no-bare-error-call) -
pkg/notify/log_test.go:90— Inside a GinkgoItblock,time.Sleep(60 * time.Millisecond)discards its return value. (rule:go-testing/no-bare-error-call) -
pkg/notify/log_test.go:150— Inside a GinkgoItblock,slog.SetDefault(slog.New(...))discards its return value. (rule:go-testing/no-bare-error-call) -
pkg/notify/log_test.go:20, 134— Twovar _ = Describe(...)blocks exist inlog_test.gobut no*_suite_test.gowithTestSuite+RunSpecsis present in the directory. Ginkgo will not discover any specs;make testexits 0 while running nothing (silent coverage loss). (rule:go-testing/suite-test-file-required) -
pkg/notify/notify.go:31—Notifierinterface is declared without a//counterfeiter:generatedirective. When the interface changes, the mock won't update until someone manually touches it — silent drift. Add//counterfeiter:generateabove the interface and rungo generate. (rule:go-testing/counterfeiter-mocks-required)
Should Fix (Important)
-
pkg/notify/telegram.go:78,88,93,101,103,106,108,110,114,122,125,128,131,135(15 occurrences) andpkg/notify/openclaw.go:57,59,67,71,80,82,85,89,92,97,107,110,113,116,120(15 occurrences) andpkg/notify/notify.go:58,60,68,71,73,76,80,83,87,96,99,102,105,109(14 occurrences) andpkg/notify/log.go:35,37,45,48(4 occurrences) — All 48 occurrences ofgo-composition/no-package-function-calls-in-business-logic(mechanical MUST from the ast-grep funnel): method bodies calllibtime.Now(),fmt.Sprintf,json.Marshal,errors.Wrapf,http.NewRequestWithContext,bytes.NewReader,io.Copy,slog.*directly. These hide the dependency graph and make the logic untestable without injecting test doubles for the stdlib/package-level calls. Wrap each capability (time, JSON, HTTP, logging) behind a small interface injected via the constructor. (rule:go-composition/no-package-function-calls-in-business-logic) -
pkg/notify/openclaw.go:97—slog.Debug("sending webhook",...)starts with uppercaseS. Log messages should start lowercase to keep the multi-source structured-log stream uniform. (rule:go-logging/lowercase-log-messages) -
pkg/notify/notify.go:87—slog.Debug("sending webhook",...)starts with uppercaseS. Same issue. (rule:go-logging/lowercase-log-messages)
Nice to Have (Optional)
None beyond the above.
✅ Correctness of the Core Fix
The actual bug fix (using libtime.Now().Sub(lastSent) instead of time.Since(lastSent) for the TTL expiry check, alongside the existing libtime.Now() stamp) is correct and well-targeted. The regression test in log_test.go is also a good addition — it patches libtime.Now directly and advances the fake clock past the TTL to assert that dedup expires, which is the exact failure mode that motivated the fix. The fix is applied consistently across all four notifier implementations (log.go, notify.go, openclaw.go, telegram.go).
Selector Mode Traceability
- Candidates: 37 judgment rules matched by Step 4b-i glob filter
- Applicable: 6 rules selected by Step 4c-sel (after filtering to rules with genuine diff-scope violations)
- Skipped: 31 rules classified as non-applicable (no matching files in diff or trigger not satisfied)
changelog/conventional-prefix-required→ not skipped — CHANGELOG.md changed with## Unreleasedbulletsgo-architecture/business-logic-not-in-main→ no main.go in diffgo-http-handler/*→ no handler files in diffgo-linting/*,go-makefile/*,go-mod-*→ no matching filesgo-prometheus/*→ no Prometheus metrics code in diff- Remaining
**/*.gorules → no violations found in judgment-tier review
Next Steps
# Fix the bare error calls in log_test.go
# Add *notify*_suite_test.go with RunSpecs
# Add //counterfeiter:generate above Notifier interface
# Consider injecting Logger and Clock interfaces to address the 48 architectural violations{
"verdict": "request-changes",
"summary": "The clock-injection bug fix is correct and well-tested, but the PR introduces 4 new MUST-violations: 3 missing HTTP response audit logs and 4 bare error calls in Ginkgo test blocks, plus a missing suite file and missing Counterfeiter directive on the Notifier interface. 48 go-composition mechanical findings also need architectural resolution.",
"comments": [
{
"file": "pkg/notify/telegram.go",
"line": 120,
"severity": "critical",
"message": "Boundary call t.httpClient.Do(req) has no log summarising the response (method + status + latency). Add slog.Debug/Info with method, path, status, latency; add error message on failure. (rule: go-logging/external-call-logs-response)"
},
{
"file": "pkg/notify/openclaw.go",
"line": 105,
"severity": "critical",
"message": "Boundary call n.httpClient.Do(req) has no log summarising the response. Add slog.Debug/Info with method, path, status, latency; add error message on failure. (rule: go-logging/external-call-logs-response)"
},
{
"file": "pkg/notify/notify.go",
"line": 94,
"severity": "critical",
"message": "Boundary call n.httpClient.Do(req) has no log summarising the response. Add slog.Debug/Info with method, path, status, latency; add error message on failure. (rule: go-logging/external-call-logs-response)"
},
{
"file": "pkg/notify/log_test.go",
"line": 29,
"severity": "critical",
"message": "buf.Reset() inside a Ginkgo It block discards its return value. Wrap in Expect(...).To(Succeed()). (rule: go-testing/no-bare-error-call)"
},
{
"file": "pkg/notify/log_test.go",
"line": 31,
"severity": "critical",
"message": "slog.SetDefault(logger) inside a Ginkgo It block discards its return value. Wrap in Expect(...).To(Succeed()). (rule: go-testing/no-bare-error-call)"
},
{
"file": "pkg/notify/log_test.go",
"line": 90,
"severity": "critical",
"message": "time.Sleep(60 * time.Millisecond) inside a Ginkgo It block discards its return value. Wrap in Expect(...).To(Succeed()). (rule: go-testing/no-bare-error-call)"
},
{
"file": "pkg/notify/log_test.go",
"line": 150,
"severity": "critical",
"message": "slog.SetDefault(slog.New(...)) inside a Ginkgo It block discards its return value. Wrap in Expect(...).To(Succeed()). (rule: go-testing/no-bare-error-call)"
},
{
"file": "pkg/notify/log_test.go",
"line": 20,
"severity": "critical",
"message": "Ginkgo Describe blocks exist (lines 20, 134) but no *_suite_test.go with TestSuite + RunSpecs is present. Ginkgo will not discover any specs (make test exits 0 with no output). Create pkg/notify/notify_suite_test.go with the standard template. (rule: go-testing/suite-test-file-required)"
},
{
"file": "pkg/notify/notify.go",
"line": 31,
"severity": "critical",
"message": "Notifier interface declared without //counterfeiter:generate directive above it. Add the directive and run go generate so mocks stay in sync with the interface. (rule: go-testing/counterfeiter-mocks-required)"
},
{
"file": "pkg/notify/telegram.go",
"line": 78,
"severity": "major",
"message": "Method body calls libtime.Now() directly — hidden time dependency. Inject via constructor as a Clock interface. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/telegram.go",
"line": 88,
"severity": "major",
"message": "Method body calls libtime.Now() directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/telegram.go",
"line": 93,
"severity": "major",
"message": "Method body calls fmt.Sprintf directly — hidden dependency. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/telegram.go",
"line": 101,
"severity": "major",
"message": "Method body calls json.Marshal directly — hidden dependency. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/telegram.go",
"line": 103,
"severity": "major",
"message": "Method body calls errors.Wrapf directly — hidden dependency. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/telegram.go",
"line": 106,
"severity": "major",
"message": "Method body calls fmt.Sprintf directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/telegram.go",
"line": 108,
"severity": "major",
"message": "Method body calls http.NewRequestWithContext and bytes.NewReader directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/telegram.go",
"line": 110,
"severity": "major",
"message": "Method body calls errors.Wrapf directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/telegram.go",
"line": 114,
"severity": "major",
"message": "Method body calls slog.Debug directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/telegram.go",
"line": 122,
"severity": "major",
"message": "Method body calls errors.Wrapf directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/telegram.go",
"line": 125,
"severity": "major",
"message": "Method body calls io.Copy directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/telegram.go",
"line": 128,
"severity": "major",
"message": "Method body calls errors.Errorf and fmt.Sprintf directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/telegram.go",
"line": 131,
"severity": "major",
"message": "Method body calls fmt.Sprintf directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/telegram.go",
"line": 135,
"severity": "major",
"message": "Method body calls slog.Info directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/openclaw.go",
"line": 57,
"severity": "major",
"message": "Method body calls libtime.Now() directly — hidden time dependency. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/openclaw.go",
"line": 59,
"severity": "major",
"message": "Method body calls slog.Debug directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/openclaw.go",
"line": 67,
"severity": "major",
"message": "Method body calls libtime.Now() directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/openclaw.go",
"line": 71,
"severity": "major",
"message": "Method body calls fmt.Sprintf directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/openclaw.go",
"line": 80,
"severity": "major",
"message": "Method body calls json.Marshal directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/openclaw.go",
"line": 82,
"severity": "major",
"message": "Method body calls errors.Wrapf directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/openclaw.go",
"line": 85,
"severity": "major",
"message": "Method body calls http.NewRequestWithContext and bytes.NewReader directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/openclaw.go",
"line": 92,
"severity": "major",
"message": "Method body calls errors.Wrapf directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/openclaw.go",
"line": 97,
"severity": "major",
"message": "Method body calls slog.Debug directly. Also starts with uppercase S — use lowercase. (rule: go-composition/no-package-function-calls-in-business-logic, go-logging/lowercase-log-messages)"
},
{
"file": "pkg/notify/openclaw.go",
"line": 107,
"severity": "major",
"message": "Method body calls errors.Wrapf directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/openclaw.go",
"line": 110,
"severity": "major",
"message": "Method body calls io.Copy directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/openclaw.go",
"line": 113,
"severity": "major",
"message": "Method body calls errors.Errorf and fmt.Sprintf directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/openclaw.go",
"line": 116,
"severity": "major",
"message": "Method body calls fmt.Sprintf directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/openclaw.go",
"line": 120,
"severity": "major",
"message": "Method body calls slog.Info directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/notify.go",
"line": 58,
"severity": "major",
"message": "Method body calls libtime.Now() directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/notify.go",
"line": 60,
"severity": "major",
"message": "Method body calls slog.Debug directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/notify.go",
"line": 68,
"severity": "major",
"message": "Method body calls libtime.Now() directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/notify.go",
"line": 71,
"severity": "major",
"message": "Method body calls json.Marshal directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/notify.go",
"line": 73,
"severity": "major",
"message": "Method body calls errors.Wrapf directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/notify.go",
"line": 76,
"severity": "major",
"message": "Method body calls http.NewRequestWithContext and bytes.NewReader directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/notify.go",
"line": 80,
"severity": "major",
"message": "Method body calls bytes.NewReader directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/notify.go",
"line": 83,
"severity": "major",
"message": "Method body calls errors.Wrapf directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/notify.go",
"line": 87,
"severity": "major",
"message": "Method body calls slog.Debug directly. Also starts with uppercase S — use lowercase. (rule: go-composition/no-package-function-calls-in-business-logic, go-logging/lowercase-log-messages)"
},
{
"file": "pkg/notify/notify.go",
"line": 96,
"severity": "major",
"message": "Method body calls errors.Wrapf directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/notify.go",
"line": 99,
"severity": "major",
"message": "Method body calls io.Copy directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/notify.go",
"line": 102,
"severity": "major",
"message": "Method body calls errors.Errorf and fmt.Sprintf directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/notify.go",
"line": 105,
"severity": "major",
"message": "Method body calls fmt.Sprintf directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/notify.go",
"line": 109,
"severity": "major",
"message": "Method body calls slog.Info directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/log.go",
"line": 35,
"severity": "major",
"message": "Method body calls libtime.Now() directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/log.go",
"line": 37,
"severity": "major",
"message": "Method body calls slog.Debug directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/log.go",
"line": 45,
"severity": "major",
"message": "Method body calls libtime.Now() directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
},
{
"file": "pkg/notify/log.go",
"line": 48,
"severity": "major",
"message": "Method body calls slog.Info directly. (rule: go-composition/no-package-function-calls-in-business-logic)"
}
],
"concerns_addressed": [
"correctness: libtime.Now().Sub(lastSent) replaces time.Since() in log.go:36 — both clock sides now use injectable clock — FIXED",
"correctness: same clock injection fix applied consistently in notify.go:59, openclaw.go:58, telegram.go:79 — FIXED",
"correctness: same clock injection fix applied to the write side (libtime.Now() stamp) in all four files — already correct, reinforced by test",
"tests: new regression spec in log_test.go advances fake clock past TTL and asserts dedup expires — ADDED (with pre-existing test quality issues)"
]
}|
Round 2 confirms the substance and blocks on the fleet's least precise rule. Merging on that basis. What round 2 verifiedIts own
That is the whole point of the PR, and the regression round 1 correctly caught is gone. What blocks it — 16 ×
|
Repo review at
2d4bfbd. 166 mechanical findings from 74 rules → 1 fixed (4 call sites).Fixed — dedup TTL was gated on the real clock
All four notifiers (
log.go,notify.go,openclaw.go,telegram.go) record dedup timestamps withtime.Now():TTL expiry is therefore only reachable by waiting — and the existing tests say so plainly. Every one of the four sleeps for exactly that reason:
libtimeis already a dependency andlibtime.Nowis a package-levelvar, so this needs no signature change on any of the four public constructors —NewNotifier(webhookURL, httpClient, dedupTTL)is unchanged. A test can now swap the clock and drive expiry deterministically.I have not rewritten the existing specs to drop their sleeps; that is a follow-up, and this change is what makes it possible.
Declined — 3 ×
no-fmt-errorfinpkg/cli/cli.goLines 65, 100 and 104.
bborbe/errorsrequires actx, and these are argument-parsing helpers with none in scope. Converting means changing signatures on the CLI surface to threadctxinto static-message validation errors — churn for no behavioural gain. Recorded rather than silently skipped.Refuted
Remaining findings fall to the standing triage table, plus the context-cancellation family assessed against the narrow form (loops iterate already-fetched in-memory collections; blocking calls take
ctxand honour it) and naming/shape suggestions identifying no defect.Verification
make precommitgreen — 8 packages, 0 lint, 0 vulns. Findings counted after dropping generated sources percommands/code-review.mdStep 1.