fix: declare zombie-sweeper fields in installed Config CRD schema - #18
Conversation
zombieSweeperIntervalSeconds and zombieJobTimeoutSeconds were declared in ConfigSpec, read live by the zombie sweeper and mirrored in AgentConfiguration, but missing from configSpecSchema() — so SetupCustomResourceDefinition overwrote the cluster CRD without them on every executor start and the API server pruned them from every Config, silently falling back to defaults. Same defect class as the v0.6.1 maxConcurrentJobs fix. Add both fields (chart minimums 10/30) plus a guard test so a future config field cannot ship missing from the CRD again. Split the schema builder into configSpecSchema/configSpecValidations/configSpecProperties to keep each function under the funlen limit.
There was a problem hiding this comment.
Now I have enough for a full adjudication. Let me produce the report.
PR Review: fix: declare zombie-sweeper fields in installed Config CRD schema
Branch:fix/crd-zombie-fields → master
Must Fix (Critical)
pkg/k8s_connector.go:122—defer close(stopCh)insideListencloses a channel received from the caller.Listenonly receives fromstopCh(via<-stopChin theselectcase); the caller is the producer. Closing a consumer-owned channel races against any goroutine still sending and panicssend on closed channel. The caller should closestopChafterListenreturns, not the callee. (rule:go-concurrency/channel-closed-by-sender-only)pkg/k8s_connector_test.go:40and line 116 — Twovar _ = Describe(...)Ginkgo spec anchors exist inpkg/k8s_connector_test.gobut no*_suite_test.gofile exists inpkg/. Ginkgo will silently passmake testwith exit 0 even though no specs ran — all test verification is bypassed. Createpkg/k8s_connector_suite_test.gowithTestSuite+RunSpecs. (rule:go-testing/suite-test-file-required)pkg/k8s_connector_test.go:99—cs.PrependReactor(...)is called inside a GinkgoItblock without checking itserrorreturn. If the reactor fails to register, the simulated error path never fires and the test silently passes against the happy path. Wrap:Expect(cs.PrependReactor(...)).To(Succeed()). (rule:go-testing/no-bare-error-call)
Should Fix (Important)
pkg/k8s_connector.go:36— The//counterfeiter:generatedirective exists aboveK8sConnectorand the mock filemocks/k8s_connector.gois present, so the mechanical finding may be a false positive or a spacing sensitivity in the AST pattern. However, to eliminate ambiguity, move the directive immediately above thetypedeclaration with no intervening blank line, and verifygo generatestill produces the mock unchanged. (rule:go-architecture/counterfeiter-directive-on-interface)pkg/k8s_connector.go:50—CRDClientBuilderis a function-type adapter implementing the functional-composition pattern and should be namedCRDClientBuilderFuncper the naming convention. (rule:go-functional-composition/func-type-name)
Nice to Have (Optional)
None.
Notes
- The core CRD schema changes (adding
zombieSweeperIntervalSecondsandzombieJobTimeoutSecondswith minimums 10/30 sourced fromagentv1constants) are correct and match the defect class fixed in v0.6.1 formaxConcurrentJobs. The refactoring ofconfigSpecSchemainto three functions (configSpecSchema,configSpecValidations,configSpecProperties) is a clean extraction. - The test assertions for the new fields (
float64(10)/float64(30)minimums) correctly match thefloat64schema type. - Precommit skipped (selector mode) — CI covers lint+test.
Traceability
- Candidates: 9 rules matched by Step 4b-i glob filter
- Applicable: 6 rules selected by Step 4c-sel (architecture-bypass: counterfeiter, concurrency; classification: suite-test, no-bare-error-call, func-type-name)
- Skipped:
changelog/conventional-prefix-required→ CHANGELOG bullet already usesfix:prefixchangelog/unreleased-entry-required→ CHANGELOG already has## Unreleasedsectiongo-composition/no-package-function-calls-in-business-logic→ k8s client calls are standard library wrappers, not hidden business logic dependencies
Selector Mode Classify Traceability
- Candidates: 9 rules matched by Step 4b-i glob filter
- Applicable: 6 rules selected by Step 4c-sel
- Skipped:
changelog/conventional-prefix-required→ CHANGELOG bullet already usesfix:prefixchangelog/unreleased-entry-required→ CHANGELOG already has## Unreleasedsectiongo-composition/no-package-function-calls-in-business-logic→ k8s client calls are standard library wrappers, not hidden business logic
{
"verdict": "request-changes",
"summary": "The CRD schema fix is correct and well-tested, but the PR introduces two must-fix issues: a channel-closed-by-wrong-goroutine concurrency bug in Listen, and missing Ginkgo suite files that silently bypass all test verification. A third must-fix flags a bare error call in a test block.",
"comments": [
{
"file": "pkg/k8s_connector.go",
"line": 122,
"severity": "critical",
"message": "defer close(stopCh) inside Listen closes a channel received from the caller. Listen only receives from stopCh (via <-stopCh in the select), so it is the consumer — the caller is the producer. A consumer must not close a channel it receives from; closing races against any goroutine still sending and panics send on closed channel. Move close(stopCh) to the caller. (rule: go-concurrency/channel-closed-by-sender-only)"
},
{
"file": "pkg/k8s_connector_test.go",
"line": 40,
"severity": "critical",
"message": "var _ = Describe(...) anchor found but no *_suite_test.go with TestSuite+RunSpecs exists in pkg/. Ginkgo will silently pass make test with exit 0 even though no specs ran — all test verification is bypassed. Create pkg/k8s_connector_suite_test.go. (rule: go-testing/suite-test-file-required)"
},
{
"file": "pkg/k8s_connector_test.go",
"line": 116,
"severity": "critical",
"message": "Second var _ = Describe(...) anchor found but no *_suite_test.go with TestSuite+RunSpecs exists in pkg/. Same silent-pass risk as the first Describe block. (rule: go-testing/suite-test-file-required)"
},
{
"file": "pkg/k8s_connector_test.go",
"line": 99,
"severity": "critical",
"message": "cs.PrependReactor(...) discards its error return inside a Ginkgo It block. If reactor registration fails the simulated error path never fires and the test silently passes against the happy path. Wrap with Expect(cs.PrependReactor(...)).To(Succeed()). (rule: go-testing/no-bare-error-call)"
},
{
"file": "pkg/k8s_connector.go",
"line": 36,
"severity": "major",
"message": "The counterfeiter directive exists and the mock file mocks/k8s_connector.go is present, so this may be a spacing sensitivity in the AST pattern. To eliminate ambiguity, move the directive immediately above the type declaration with no intervening blank line and verify go generate produces an unchanged mock. (rule: go-architecture/counterfeiter-directive-on-interface)"
},
{
"file": "pkg/k8s_connector.go",
"line": 50,
"severity": "major",
"message": "CRDClientBuilder is a function-type adapter implementing the functional-composition pattern and should be named CRDClientBuilderFunc per the naming convention. (rule: go-functional-composition/func-type-name)"
}
],
"concerns_addressed": [
"correctness: crdMinZombieSweeperInterval/crdMinZombieJobTimeout sourced from agentv1 package constants — verified, constants exist and equal 10/30",
"correctness: configSpecSchema refactored into three functions — call sites verified, desiredCRDSpec still composes correctly",
"tests: new test uses float64(10)/float64(30) matching the schema minimums — verified correct"
]
}bborbe
commented
Aug 17, 2026
Reviewing the six findings — three are pre-existing code not touched by this PR, and three are factually incorrect (cannot be fixed as suggested): 1. 2. Missing 3. 4. Counterfeiter directive spacing (major) — As the review itself notes, likely a false positive: the mock file 5. Net: this PR adds two schema fields + a guard test, all verified by the review's own |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Now I have enough context. Let me do the in-session adjudication for selector mode.
Step 4c-sel CLASSIFY — evaluating candidate judgment rules against the diff:
| Rule | Decision | Reason |
|---|---|---|
go-testing/suite-test-file-required | applicable | pkg/k8s_connector_test.go uses Ginkgo but has no *_suite_test.go companion |
go-concurrency/channel-closed-by-sender-only | applicable | pkg/k8s_connector.go:121 — Listen closes stopCh but receives it from its caller; caller owns the channel |
Step 4d-sel ADJUDICATE — reading relevant rule blocks and evaluating:
Rule: go-testing/suite-test-file-required — pkg/k8s_connector_test.go has two Ginkgo var _ = Describe(...) blocks at lines 40 and 116 with no companion *_suite_test.go in pkg/. Without it Ginkgo silently passes without discovering specs.
Rule: go-concurrency/channel-closed-by-sender-only — At pkg/k8s_connector.go:121, close(stopCh) is called inside Listen, but stopCh is received as a parameter from the caller. The caller creates and owns the channel; Listen is a consumer of the channel (it receives it, passes it to factory.Start). Closing a channel from the consumer side races against any concurrent send on that channel from the caller side.
Mechanical findings review — 22 of the 27 findings are pre-existing go-composition/no-package-function-calls-in-business-logic violations that exist throughout k8s_connector.go unchanged by this PR. Since the PR's purpose is adding zombie-sweeper fields (not rearchitecting k8s client injection), those are informational. The one genuinely new finding from the diff is the zombieSweeperIntervalSeconds/zombieJobTimeoutSeconds block — correctly structured, minimums match agentv1 constants.
Additional review findings — CRDClientBuilder (line 49) is a function-type alias named without the required Func suffix per go-functional-composition/func-type-name.
PR Review Report
Must Fix (Critical)
None — no security, data correctness, or transaction issues introduced.
Should Fix (Important)
pkg/k8s_connector.go:121—close(stopCh)insideListenviolates the sender-owns-channel convention.stopChis received as a parameter from the caller, who creates and owns it. Closing from the receiver side races against any concurrent send from the caller. The caller should closestopChafterListenreturns. (rule:go-concurrency/channel-closed-by-sender-only)pkg/k8s_connector_test.go:40,116— Both GinkgoDescribeblocks lack a companion*_suite_test.gowithTestSuite+RunSpecs. Without it,make testexits 0 with zero specs discovered (silent coverage loss). Createpkg/k8s_connector_suite_test.go. (rule:go-testing/suite-test-file-required)pkg/k8s_connector.go:49—CRDClientBuilderis a function-type adapter but not namedCRDClientBuilderFunc. Matches the pattern//counterfeiter:generate -o ../mocks/k8s_connector.go --fake-name FakeK8sConnector . K8sConnectorcorrectly, but the type alias itself violates theXxxFuncnaming convention, breakinggrep -n "CRDClientBuilderFunc"discoverability. (rule:go-functional-composition/func-type-name)
Nice to Have (Optional)
CHANGELOG.md:5— The new bullet correctly describes the bug and its class (same class as the v0.6.1maxConcurrentJobsfix). Well-written and actionable.
Selector Mode Traceability
- Candidates: ~25 judgment rules from
rules/index.json; evaluated file-scope triggers - Applicable: 2 rules (
go-testing/suite-test-file-required,go-concurrency/channel-closed-by-sender-only) - Skipped: All other judgment rules —
applies_whenconditions do not match changed files
Notes
The core bug fix is correct and well-guarded:
zombieSweeperIntervalSeconds(min 10) andzombieJobTimeoutSeconds(min 30) are correctly added toconfigSpecProperties()- The
crdMinZombieSweeperInterval/crdMinZombieJobTimeoutpackage vars correctly referenceagentv1.MinZombieSweeperIntervalSeconds(10) andagentv1.MinZombieJobTimeoutSeconds(30) - The test at
pkg/k8s_connector_test.go:152asserts both fields with the correct minimums, matching the admission guards ink8s/apis/agent.benjamin-borbe.de/v1/types.go - The structural refactor (
configSpecSchema→configSpecSchema/configSpecValidations/configSpecProperties) is clean and improves maintainability
The 22 pre-existing go-composition/no-package-function-calls-in-business-logic findings in k8s_connector.go are architectural debt in the existing code, not introduced by this PR.
{
"verdict": "request-changes",
"summary": "The zombie-sweeper CRD fix is correct and well-tested, but two issues from the mechanical funnel need addressing before merge: (1) Listen closes its caller-owned stopCh, violating sender-only channel conventions, and (2) k8s_connector_test.go lacks a Ginkgo suite file causing silent spec non-discovery. A minor naming convention violation on the CRDClientBuilder type alias also warrants a fix.",
"comments": [
{
"file": "pkg/k8s_connector.go",
"line": 121,
"severity": "major",
"message": "close(stopCh) is called inside Listen, but stopCh is received as a parameter from the caller who creates and owns it. Closing from the consumer side races against any concurrent send from the caller. The caller should close stopCh after Listen returns. (rule: go-concurrency/channel-closed-by-sender-only)"
},
{
"file": "pkg/k8s_connector_test.go",
"line": 40,
"severity": "major",
"message": "Ginkgo Describe block found but no companion *_suite_test.go with TestSuite + RunSpecs exists in pkg/. Without it make test exits 0 with zero specs discovered. Create pkg/k8s_connector_suite_test.go. (rule: go-testing/suite-test-file-required)"
},
{
"file": "pkg/k8s_connector_test.go",
"line": 116,
"severity": "major",
"message": "Ginkgo Describe block found but no companion *_suite_test.go with TestSuite + RunSpecs exists in pkg/. Without it make test exits 0 with zero specs discovered. Create pkg/k8s_connector_suite_test.go. (rule: go-testing/suite-test-file-required)"
},
{
"file": "pkg/k8s_connector.go",
"line": 49,
"severity": "nit",
"message": "CRDClientBuilder is a function-type adapter but not named CRDClientBuilderFunc, violating the XxxFunc naming convention. (rule: go-functional-composition/func-type-name)"
}
],
"concerns_addressed": [
"correctness: structural refactor of configSpecSchema into configSpecSchema/configSpecValidations/configSpecProperties — verified, no logic changes, only structural split",
"correctness: new integer minimums (10 and 30) match agentv1 constants (MinZombieSweeperIntervalSeconds=10, MinZombieJobTimeoutSeconds=30) and admission guards in types.go — verified consistent",
"correctness: crdMinZombieSweeperInterval/crdMinZombieJobTimeout reference agentv1 constants — verified, constants exist with correct values"
]
}
What
zombieSweeperIntervalSecondsandzombieJobTimeoutSecondswere declared inConfigSpec, read live by the zombie sweeper (zombie_sweeper.go) and mirrored inAgentConfiguration, but missing fromconfigSpecSchema()inpkg/k8s_connector.go.SetupCustomResourceDefinitionoverwrites the cluster CRD on every executor start, so the missing fields were pruned from every Config by the API server — the sweeper silently fell back to defaults. Same defect class as the v0.6.1maxConcurrentJobsfix (see #17).Change
zombieSweeperIntervalSeconds(min 10) andzombieJobTimeoutSeconds(min 30) to the installed CRD schema, mirroringhelm/crds/config-crd.yamland the v1 types admission guards.configSpecSchemaintoconfigSpecSchema/configSpecValidations/configSpecPropertiesto keep each function under the funlen limit.Verify
make precommitpasses. After deploy, the live CRD should show all 16 fields matching chart 0.5.2.