Skip to content

feat(tracker): ApplyTrackerFacts reducer + shared observer skeleton (#112)#116

Merged
harshitsinghbhandari merged 5 commits into
mainfrom
feat/issue-112
Jun 7, 2026
Merged

feat(tracker): ApplyTrackerFacts reducer + shared observer skeleton (#112)#116
harshitsinghbhandari merged 5 commits into
mainfrom
feat/issue-112

Conversation

@harshitsinghbhandari
Copy link
Copy Markdown
Collaborator

Closes #112.

Lands the two scaffolding pieces from the issue so the Tracker observer (issue #35) and its provider adapters can be built in small, reviewable follow-ups instead of one monolithic PR.

Part 1 — ports.TrackerObservation DTO + lifecycle.Manager.ApplyTrackerFacts

  • New backend/internal/ports/tracker_observations.go mirrors scm_observations.go's field-level documentation style: Fetched bool, ObservedAt time.Time, Provider/Host/Repo, normalized TrackerIssueObservation, Comments, and the Changed{State, Assignee, Comments} discriminator.
  • lifecycle.Manager.ApplyTrackerFacts in backend/internal/lifecycle/reactions.go mirrors ApplySCMObservation shape: Fetched gate → terminal-state shortcut → per-bucket reactions. Three reactions:
  • Unit tests in backend/internal/lifecycle/manager_test.go cover terminate (done + cancelled), log-only assignee, nudge fires, nudge suppressed on repeat, new bot id refires, not-fetched no-op, terminated-session no-op.

No observer is wired in this PR — that's intentional. The contract is the deliverable.

Part 2 — Shared observer skeleton extraction

  • New backend/internal/observe package (one-paragraph package doc on top of observer.go) carries the observer-pattern-general pieces:
    • StartPollLoop(ctx, tick, poll, logger, name) — immediate first poll + ticker + ctx-done exit. SCM Observer.Start now delegates to it.
    • CredentialProbe + CheckCredentialsOnce(ctx, probe, checked, disabled, logger, name) — lazy first-poll credential gate. The SCM observer keeps credentialsChecked/disabled as Observer fields; the shared helper mutates them via pointer so state ownership stays single-source.
    • CacheSet[V any](m, order, max, key, value) + CacheDelete[V any](m, order, key) — one generic bounded-FIFO helper collapses the three near-identical cacheSet{String,Time,Bool} bodies and the standalone evictStrings. The SCM-side methods are kept as one-line wrappers that thread o.Cache.max through, so call sites and tests are untouched.
  • backend/internal/observe/observer_test.go adds 11 small unit tests for the shared package, including a CacheSet-over-time.Time case to exercise the generic.

SCM behavior is unchanged. Both the existing 21-test SCM suite and the end-to-end test added in PR #115 stay green without modification.

Acceptance checklist (from issue body)

  • ports.TrackerObservation + TrackerChanged exist with field-level documentation matching the SCM DTO style.
  • lifecycle.Manager.ApplyTrackerFacts exists as a working reducer with the three initial reactions covered by unit tests.
  • Shared observer helpers (goroutine supervisor, credential gate, bounded FIFO cache) live in a shared package and the SCM observer consumes them. SCM 21-test suite stays green.
  • go build ./... && go test -race ./... clean (585 tests, 39 packages).

Validation

cd backend && go build ./... && go test -race ./...

→ 585 tests pass across 39 packages.

🤖 Generated with Claude Code

@harshitsinghbhandari harshitsinghbhandari added this to the rewrite milestone Jun 5, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 5, 2026

Greptile Summary

This PR lands two scaffolding pieces ahead of the Tracker observer (#35): the ports.TrackerObservation DTO and the lifecycle.Manager.ApplyTrackerFacts reducer, plus extraction of shared observer primitives (StartPollLoop, CheckCredentialsOnce, CacheSet/CacheDelete) into a new observe package consumed by the SCM observer.

  • ApplyTrackerFacts mirrors ApplyPRObservation's structure — Fetched gate → terminal-state shortcut (MarkTerminated) → session-read → ActivityWaitingInput/terminated guards → per-bucket reactions (log-only assignee, bot-comment dedup nudge via sendOnce with empty prURL for in-memory-only dedup). Two bugs found in review (CheckCredentialsOnce disabled short-circuit returning wrong value; bot-comment && vs || filter) were fixed before the review completed, and regression tests for both were added.
  • Shared observe package collapses three near-identical cacheSet* bodies and the loop/checkCredentials duplication; newCache always normalises max to DefaultCacheMax (512), so CacheSet's maxEntries <= 0 guard is unreachable through the normal API and introduces no behavioural change in the SCM path.

Confidence Score: 5/5

Safe to merge — all changed paths are either new scaffolding with no live callers yet, or refactors of the SCM observer that preserve identical runtime behaviour.

The two bugs surfaced in earlier review rounds were both fixed and covered by regression tests before this review. The SCM refactor is behaviourally neutral. No new issues were found.

No files require special attention.

Important Files Changed

Filename Overview
backend/internal/ports/tracker_observations.go New provider-neutral TrackerObservation DTO mirroring SCMObservation; field-level docs are thorough and the shape is consistent with the existing contract.
backend/internal/lifecycle/reactions.go Adds ApplyTrackerFacts reducer with terminal-state termination, assignee-log, and bot-nudge paths; dedup filter now correctly uses
backend/internal/lifecycle/manager_test.go Eight new unit tests cover all reaction paths including the empty-ID regression test added to address a prior review finding.
backend/internal/observe/observer.go New shared observe package extracting StartPollLoop, CheckCredentialsOnce (disabled short-circuit now returns !*disabled), and generic CacheSet/CacheDelete helpers.
backend/internal/observe/observer_test.go 11 unit tests covering cache, credential gate (including regression test for disabled short-circuit), and poll-loop; both spin-wait loops correctly capture deadline before the loop body.
backend/internal/observe/scm/observer.go SCM observer loop, checkCredentials, and three cacheSet* methods collapsed to one-line wrappers; behaviour unchanged since newCache normalises max to DefaultCacheMax (512).

Reviews (3): Last reviewed commit: "test(observe): capture deadline once in ..." | Re-trigger Greptile

Comment thread backend/internal/observe/observer.go
Comment thread backend/internal/lifecycle/reactions.go Outdated
harshitsinghbhandari added a commit that referenced this pull request Jun 5, 2026
…ent filter

Two P1 review findings on #116:

1. observe.CheckCredentialsOnce was returning (true, nil) on every
   call after the gate ran, even when the probe had marked the
   observer disabled, because the *checked short-circuit ignored
   *disabled. The SCM observer didn't surface this in practice — its
   Poll method has an independent `if o.disabled { return nil }`
   guard that runs first — but a future Tracker observer that relies
   on the helper's documented contract ("Observer stays disabled")
   would silently flip back to "credentials available" after the
   first poll. Change the short-circuit to `return !*disabled, nil`
   and lock the behavior with a regression test that issues repeat
   calls after the probe reported unavailable.

2. lifecycle.newBotCommentContent's "skip uninteresting comments"
   filter used && where it needed ||. A bot comment with an empty ID
   but a non-empty body slipped through and appended "" to the ids
   slice. If every bot comment in the observation had an empty ID,
   strings.Join produced "" — which matches the zero value of the
   in-memory dedup map, so sendOnce treated the nudge as
   already-sent and silently suppressed it forever. Switch to || so
   any comment missing either an ID or a body is dropped, and add a
   regression test that an empty-ID bot comment never nudges (and
   does not pollute the dedup state for a follow-up comment that has
   a real ID).

586 tests pass with -race.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Comment thread backend/internal/observe/observer_test.go Outdated
harshitsinghbhandari and others added 5 commits June 7, 2026 21:22
Move the observer-pattern-general pieces of the SCM observer into a new
backend/internal/observe package so the tracker observer (issue #35) can
build on the same primitives:

- StartPollLoop: goroutine supervisor with immediate-first-poll + ticker
  + ctx-done exit. SCM Observer.Start now delegates to it.
- CheckCredentialsOnce: lazy first-poll credential gate driven by a
  CredentialProbe closure. SCM observer keeps credentialsChecked/disabled
  as Observer fields; the shared helper mutates them via pointer so
  state ownership stays single-source.
- CacheSet[V any] / CacheDelete[V any]: one generic bounded-FIFO helper
  replaces the three near-identical cacheSet{String,Time,Bool} bodies
  and the standalone evictStrings. The SCM-side methods are now
  one-line wrappers that thread o.Cache.max into the shared helper, so
  existing call sites and tests are untouched.

SCM behavior is unchanged. The full 21-test SCM suite (including the
end-to-end test added in PR #115) plus 577 backend tests stay green
under `go test -race`.

Part of #112.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Land the contract that the future Tracker observer (issue #35) and its
provider adapters must satisfy. No observer is wired in this PR — the
DTO + reducer are the deliverable, and locking the shape now lets the
observer + adapter work happen in small follow-up PRs.

DTO (backend/internal/ports/tracker_observations.go):
- TrackerObservation mirrors ports.SCMObservation: Fetched bool,
  ObservedAt time.Time, Provider/Host/Repo, normalized Issue facts,
  Comments, and a Changed{State, Assignee, Comments} discriminator.
- TrackerIssueObservation carries the minimal facts lifecycle needs
  today (state, assignee, title, body, timestamps); richer
  per-provider metadata stays inside each adapter.
- TrackerCommentObservation carries the comment fields needed for the
  bot-mention nudge (Author, Body, IsBot, ID for dedup).

Reducer (backend/internal/lifecycle/reactions.go):
- ApplyTrackerFacts(ctx, sessionID, ports.TrackerObservation) error,
  mirroring ApplySCMObservation's "Fetched gate → terminal-state →
  per-bucket reactions" shape.
- Three initial reactions:
    * Issue state == done | cancelled → MarkTerminated (idempotent).
    * Changed.Assignee → log only via slog.Default(). The "assignee
      changed away from AO" policy is reserved for #40.
    * Changed.Comments with bot comments → one-time nudge with
      strings.Join'd bot bodies, deduped by comment IDs.
- The nudge path reuses sendOnce with an empty prURL so the in-memory
  dedup applies but the PR-row persistence path is skipped. Tracker
  signature persistence will land with #35 alongside issue-row storage.

Tests in backend/internal/lifecycle/manager_test.go cover each branch:
terminate (done + cancelled), log-only assignee, nudge fires on new
bot comment, nudge suppressed on repeat, new bot comment id refires,
not-fetched is no-op, terminated session ignores observations.

Part of #112.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
golangci-lint revive flagged the CacheSet generic helper's max
parameter as shadowing the built-in max() function. Rename to
maxEntries; signature change is internal to the observe package and
the SCM observer's one-line wrappers pass the value positionally, so
no call sites need updating.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ent filter

Two P1 review findings on #116:

1. observe.CheckCredentialsOnce was returning (true, nil) on every
   call after the gate ran, even when the probe had marked the
   observer disabled, because the *checked short-circuit ignored
   *disabled. The SCM observer didn't surface this in practice — its
   Poll method has an independent `if o.disabled { return nil }`
   guard that runs first — but a future Tracker observer that relies
   on the helper's documented contract ("Observer stays disabled")
   would silently flip back to "credentials available" after the
   first poll. Change the short-circuit to `return !*disabled, nil`
   and lock the behavior with a regression test that issues repeat
   calls after the probe reported unavailable.

2. lifecycle.newBotCommentContent's "skip uninteresting comments"
   filter used && where it needed ||. A bot comment with an empty ID
   but a non-empty body slipped through and appended "" to the ids
   slice. If every bot comment in the observation had an empty ID,
   strings.Join produced "" — which matches the zero value of the
   in-memory dedup map, so sendOnce treated the nudge as
   already-sent and silently suppressed it forever. Switch to || so
   any comment missing either an ID or a body is dropped, and add a
   regression test that an empty-ID bot comment never nudges (and
   does not pollute the dedup state for a follow-up comment that has
   a real ID).

586 tests pass with -race.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The `TestStartPollLoop_LogsPollErrorWithoutPanic` spin-wait was
computing the loop bound as `time.Now().Before(time.Now().Add(200ms))`
on every iteration, which is permanently true — the loop could only
exit via the `break`. Under a scheduler delay (heavy CI load or
`GOMAXPROCS=1`) where two polls never land in time, the test would
hang until the wall-clock kill rather than failing fast.

Capture the deadline once before the loop, and tighten the assertion
to actually require two polls + done-channel closure within a bounded
window, matching `TestStartPollLoop_FirstPollImmediateThenTicks`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

harshitsinghbhandari has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@harshitsinghbhandari harshitsinghbhandari merged commit 33a1ee6 into main Jun 7, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tracker: ApplyTrackerFacts reducer scaffolding + shared observer skeleton extraction

1 participant