refactor(backend): LLD maintainability fixes in controllers/service layers (#95)#96
Conversation
…ayers Addresses the high + medium severity findings from the LLD review of backend/internal/httpd and backend/internal/service (#95): 1. Controllers no longer import internal/session_manager. Session sentinel errors are now *domain.ServiceError values carrying their own HTTP mapping, so the controller translates them with one generic errors.As — no cross-package sentinel imports. 2. One error pattern across services: project.Error is now an alias of the shared domain.ServiceError, and session_manager sentinels use it too. A single writeServiceError replaces the per-resource error switches. 3. Clean-orchestrator business logic moved out of the controller into session.Service.SpawnOrchestrator(ctx, projectID, clean). 4. isGitRepo no longer treats case-different paths as equal on case-sensitive filesystems; case-insensitive compare is gated to darwin/windows via samePath. 5. Project repo check sits behind an injectable GitChecker, so the service is testable without a real git binary. 6. httpd exports only the production constructors (NewWithDeps, NewRouterWithControl); the 3 test-only wrappers are removed and the "router with empty deps" convenience moved to an unexported test helper. Closes #95 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the domain.ServiceError approach with a REST-API-scoped error package and a single envelope renderer, per review feedback: - Add internal/httpd/errors (package errors, aliased apierr): one structured Error type with semantic Kinds (Internal/Invalid/NotFound/Conflict) and constructors. Imports nothing, so any layer can depend on it. - envelope.WriteError is now the single path from a service error to the wire APIError, and the only place a Kind becomes an HTTP status/word. The per-resource writeProjectError/writeSessionError translators are gone. - Delete domain/errors.go (keeps domain pure of HTTP-flavored kinds) and service/project/errors.go (no per-service error files); services build errors inline via apierr constructors. - session_manager sentinels are apierr.Error values (pointer identity still works with errors.Is). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…change Defer findings #4 (isGitRepo case-sensitivity) and #5 (GitChecker seam) out of this PR. Restores the original exec-based isGitRepo and the New(store) constructor; removes git.go, git_test.go, and the test-only export shims. The error-standardization and other findings are unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The session_manager is the internal command engine and must not depend on the REST API error vocabulary. Revert its sentinels to plain errors.New values and move the engine→API translation into the service/session facade (toAPIError), which is the correct boundary. Controllers still see apierr.Error and never import the engine; the engine no longer imports internal/httpd/errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@greptileai Review |
Greptile SummaryThis PR consolidates the backend's error handling story into a single
Confidence Score: 5/5Safe to merge — the refactoring is a direct mechanical substitution with no behavioral changes to HTTP status codes or error codes on any path. All five session_manager sentinel-to-status mappings in toAPIError match the removed writeSessionError switch case for case. All project service error kind strings map to the same HTTP statuses via the new apierr.Kind enum. The UpsertProject path that previously passed raw store errors through is now consistently wrapped as apierr.Internal. The commander interface and new service-layer unit tests tighten coverage without introducing new risks. No files require special attention. Important Files Changed
Sequence DiagramsequenceDiagram
participant C as Controller
participant S as Service
participant SM as session_manager.Manager
participant EW as envelope.WriteError
C->>S: SpawnOrchestrator / Spawn / Kill / etc.
S->>SM: Spawn / Kill / Restore / Send
SM-->>S: error sentinel or raw
S->>S: toAPIError via errors.Is
S-->>C: apierr.Error or raw error
C->>EW: WriteError(w, r, err)
EW->>EW: errors.As for apierr.Error
alt apierr.Error found
EW-->>C: httpStatus Kind maps to 400 404 409 500
else fallback
EW-->>C: 500 INTERNAL_ERROR
end
Reviews (4): Last reviewed commit: "refactor(apierr): rename package, test S..." | Re-trigger Greptile |
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review (local; verdict: approve-with-nits)Cleanly addresses #95 High + Medium. Build/vet/race-tests green locally on Coverage of #95
Concerns
Recommended actions (priority order)
Scope discipline is good — the explicit |
Reconcile main's new session code (rename, cleanup, orchestrator list/get, display name) with this branch's error refactor: main's handlers now use envelope.WriteError; service.Rename returns apierr errors; the getOrchestrator not-found is written directly via envelope (no session_manager import in the controller). Tests/fakes updated to the apierr contract. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address review feedback on PR #96: - Rename internal/httpd/errors → internal/httpd/apierr (package apierr) so importers no longer alias around the stdlib errors package. - Add a commander seam to session.Service and unit-test the relocated clean-orchestrator rule: clean=true kills all active orchestrators before spawning; clean=false spawns without kills. - project.Add: wrap the UpsertProject store error in apierr.Internal for parity with its sibling paths (was a raw 500). - Document that KindInternal is iota's zero value, so a zero-value Error defaults to 500. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks for the thorough review — addressed in
|
harshitsinghbhandari
left a comment
There was a problem hiding this comment.
Approving — final pass on `fead016`.
All 5 actionable items from my earlier comment are in:
- ✅ `Service.SpawnOrchestrator(clean=true)` covered by
TestSpawnOrchestratorCleanKillsActiveOrchestratorsBeforeSpawn(asserts kill-before-spawn ordering, leaves the worker and terminated orchestrator alone) +TestSpawnOrchestratorNoCleanSkipsKills. The unexportedcommanderseam (service.go:31-39) is the right shape for this — internal, no production surface change. - ✅ Package renamed
internal/httpd/errors→internal/httpd/apierr(package apierr); no more alias dance for callers. - ✅ Const block on
Kindnow documents thatKindInternal = iotameans a zero-valueErrordefaults to 500. - ✅
project.Addwraps theUpsertProjectstore error withapierr.Internal("PROJECT_ADD_FAILED", …)for parity with sibling paths. - ✅ PR description's session_manager bullet corrected to reflect what the code actually does (sentinels stay as
errors.New;service/session/toAPIErroris the translation boundary).
The log.Error on envelope.WriteError's fallthrough was reasonably deferred — envelope carries no logger today and it's out of #95's scope.
Local: go build ./... clean, race-tests green across httpd/..., service/..., session_manager/.... CI is 7/7 SUCCESS on fead016. Nothing blocking.
What & why
Implements the High + Medium severity maintainability findings from the LLD review of
backend/internal/httpd(controllers/router/server) andbackend/internal/service. Partially addresses #95.Changes
internal/session_manager. Controllers render structured errors generically — the cross-package sentinel imports are gone.session.Service.SpawnOrchestrator(ctx, projectID, clean). The controller just validates input and delegates; the rule is unit-tested at the service.NewWithDeps,NewRouterWithControl). The 3 test-only wrappers are removed; the "router with empty deps" convenience is an unexportednewTestRouterhelper.Error standardization (finding #2)
The error story collapses to one type + one renderer, scoped to the REST API:
internal/httpd/apierr(package apierr) — a single structuredError{Kind, Code, Message, Details}with semanticKinds (KindInternal/KindInvalid/KindNotFound/KindConflict), not HTTP words. Constructors:Invalid/NotFound/Conflict/Internal. The package imports nothing.envelope.WriteError(w, r, err)is the single path from any service error to the lockedAPIErrorwire shape, and the only place aKindis mapped to an HTTP status/word. The per-resourcewriteProjectError/writeSessionErrortranslators are deleted.errors.goand nothing indomain. Services build errors inline viaapierr.*.domainstays free of HTTP-flavored kinds.session_managerkeeps plainerrors.Newengine sentinels — the internal command engine does not depend on the API error vocabulary. Theservice/sessionfacade (toAPIError) is the single boundary that translates those engine sentinels intoapierrerrors before they reach a controller.Layering note (deliberate)
The error vocabulary lives under
internal/httpd/, so the service facades (service/project,service/session) import thehttpdtree — the conscious "these services exist to serve the REST API" coupling, fine given the CLI is a thin HTTP client. The internal engine (session_manager) does not import it. No import cycle:httpd/apierris a leaf (imports nothing internal), verified withgo list -deps.Acceptance criteria
internal/session_managerDeferred to #97:
isGitRepocase-sensitivity fix; project service testable without a real git binary.Testing
go build ./...,go vet ./...,gofmt -lcleangolangci-lint run ./...(v2.12.2, the CI linter): 0 issuesgo test ./...passes except one pre-existing, environment-specific failure ininternal/terminal(zellij IPC socket path too long under macOS$TMPDIR) — unrelated to these changesmain(PRs feat(agent): opencode adapter + activity plugin hooks #80/feat: add issue #90 light backend CLI commands #98/feat(cli): enrich ao doctor (#90) #99), with main's new session code (rename, cleanup, orchestrator list/get) reconciled to theapierrpattern🤖 Generated with Claude Code