Write a service unit test (agent-manager-service)
Fast, DB-free tests of service logic with collaborators mocked.
Read first: agent-manager-service/AGENTS.md → the "Testing" section (two tiers, what goes where, mocks, CI lint traps). This skill is the executable checklist.
Rules that decide correctness
- File:
services/<service>_unit_test.go,package services, no build tag — omit the//go:buildline entirely. A//go:build integrationline makes it an integration (DB) test. - Only test the service layer. Never unit-test the repository (mocking a repo to test the repo is circular) — that's the integration tier.
- Inject mocks via the
NewXxx(...)constructor, not by reaching into fields. - Assert the service's own logic — error mapping, validation gates, branching, fan-out. Use
assert.ErrorIs/assert.NotErrorIsfor sentinels, and explicitly check that a real error is not masked as not-found.
Mocks
- Repositories →
repomocks.<Iface>Mock; clients →clientmocks.<Iface>Mock(bothmoq-generated; runmake codegenif the interface is new). - Configure
<Method>Funcfields. Leaving a funcniland having it called panics — use that deliberately to assert a path must not be reached. - In-package interfaces with no generated mock (e.g.
MonitorExecutor,GitCredentialsService) → hand-write a func-field stub in the test file, same<Method>Funcshape. - Reuse shared helpers, don't redeclare (duplicate = compile error):
strPtr(inllm_deployment_service_test.go),discardLogger(inevaluator_manager_unit_test.go).
Reference
Copy the shape of services/agent_kind_service_unit_test.go.
CI lint traps (CI lints test files too, with .github/linters/.golangci.yaml)
nilnil— neverreturn nil, nil. Return an empty typed value (return []*models.Foo{}, nil). If(nil, nil)is genuinely under test://nolint:nilnil // <reason>.goheader— every.gofile (tests included) needs the Apache license header; copy from an existing file.errorlint/nilerr— compare witherrors.Is, not==; don'treturn nilafter a non-nil error check.nolintlint— a bare//nolintis itself an error; always//nolint:<linter> // <reason>.exhaustructis off for_test.go(partial struct literals fine in fixtures) but still applies to production code you touch.
Run
make test-unit # runs unit tier + sets required config env vars
# single test (env vars load at import time):
go test -run 'TestAgentKindService' ./services/ # needs DB_*/OPEN_CHOREO_BASE_URL/ENCRYPTION_KEY/SERVER_PORT set
Services that sign tokens need make gen-keys first.
Services that emit audit events
Operations that must not happen unrecorded — minting a token, rotating a key, deploying, deleting — refuse to proceed when no audit recorder is installed. A bare context.Background() therefore makes them fail with audit: recorder unavailable.
Pass auditableCtx(t) (declared in services/audit_testing_test.go, do not redeclare it):
resp, err := svc.RotateAPIKey(auditableCtx(t), ouID, proj, agent, env, keyName, req)
To assert the refusal itself, pass a bare context and expect audit.ErrRecorderUnavailable.
Done checklist
-
make test-unitpasses. -
go build -tags=integration ./...compiles (catches helper-name collisions across tiers). - CI lint clean:
golangci-lint run --config .github/linters/.golangci.yaml ./... -
gofmt -lclean on changed files.