You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add a comprehensive unit test suite in pkg/vmcp/cli/ for the Serve and Validate entry points and their helper functions (loadAndValidateConfig, discoverBackends, createSessionFactory, loadAuthServerConfig). Tests use go.uber.org/mock (gomock) and write temporary config files to disk; no running server or Kubernetes cluster is required. This work was deliberately separated from #4879 to keep each PR within the 400-line / 10-file review limit.
Context
#4879 creates pkg/vmcp/cli/ as a new library package, but intentionally defers thorough unit test coverage to keep its PR size manageable. This item delivers that coverage. Because pkg/vmcp/cli/ is business logic (not a thin CLI wrapper), organizational standards require thorough unit tests alongside the source files — the same principle that governs every other package under pkg/. The tests validate the config loading pipeline, validation rejection paths, backend discovery wiring, session factory HMAC-secret behavior (including the Kubernetes fail-fast rule), and the auth server config side-loading logic.
Dependencies: Depends on #4879 (the pkg/vmcp/cli/ package must exist before tests can be written against it) Blocks: (none — can merge independently after #4879)
Acceptance Criteria
pkg/vmcp/cli/serve_test.go and pkg/vmcp/cli/validate_test.go exist, each carrying the required SPDX copyright header
loadAndValidateConfig unit tests cover: valid config round-trip, missing config file (error), malformed YAML (error), and config that fails semantic validation (error)
discoverBackends unit tests cover: static mode (non-empty cfg.Backends slice → NewUnifiedBackendDiscovererWithStaticBackends path) and empty-backends warning path
createSessionFactory unit tests cover: HMAC secret set via env var (factory created successfully), no secret + non-Kubernetes environment (factory created with warning), and no secret + Kubernetes environment (error returned)
Validate unit tests cover: missing config path (error), valid config file (nil return), and invalid config (error)
All test cases use t.Cleanup for teardown (not defer) and random ports via net.Listen("tcp", ":0") where port allocation is needed
Mocks for aggregator.Aggregator, aggregator.BackendDiscoverer, and vmcpauth.OutgoingAuthRegistry are sourced from the existing generated mocks in pkg/vmcp/aggregator/mocks/ and pkg/vmcp/client/mocks/; no hand-written mocks are introduced
go test ./pkg/vmcp/cli/... passes with no failures
go vet ./pkg/vmcp/cli/... passes with no issues
No new external Go module dependencies are introduced
All tests pass
Code reviewed and approved
Technical Approach
Recommended Implementation
Write two test files (serve_test.go and validate_test.go) in package cli (same package as the source, allowing access to unexported helpers if needed) or package cli_test (external test package). Prefer package cli_test unless a helper function is unexported and cannot be tested through the exported API — in that case use package cli to keep coverage complete.
For each function under test:
Write config YAML into a t.TempDir() path using os.WriteFile.
Call the function under test directly (no Cobra command invocation needed).
Assert the returned error or value with require.NoError / require.Error / require.ErrorContains from github.com/stretchr/testify/require.
For createSessionFactory, manipulate the VMCP_SESSION_HMAC_SECRET environment variable with t.Setenv (automatically restored after test). Mock runtime.IsKubernetesRuntime() behavior by setting KUBERNETES_SERVICE_HOST env var (the standard signal the container runtime package uses to detect Kubernetes context).
For discoverBackends, the dynamic Kubernetes discovery path inside discoverBackends requires a live API server and is therefore not unit-testable. Focus unit tests on the static mode path (non-empty cfg.Backends) which is the relevant path for the local vMCP use case. The dynamic path is covered by existing integration tests in test/integration/vmcp/.
Patterns & Frameworks
SPDX headers: Every new .go file must open with // SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc. and // SPDX-License-Identifier: Apache-2.0
Test assertions: github.com/stretchr/testify/require for fatal assertions (fail immediately), assert for non-fatal checks in table-driven subtests
Table-driven tests: Use for _, tc := range tests { t.Run(tc.name, func(t *testing.T) {...}) } for functions with multiple input variants (config loading, validation, HMAC secret scenarios)
Temp directories: Use t.TempDir() for config files — automatically cleaned up after each test
Env var manipulation: Use t.Setenv(key, value) — automatically restores the original value after the test
Teardown: Use t.Cleanup(func() {...}) for any resources allocated inside tests, not defer
Port allocation: Use net.Listen("tcp", ":0") when a free port is needed; close the listener before passing the port
Gomock: Initialize with ctrl := gomock.NewController(t) (no manual ctrl.Finish() needed — testify handles this automatically via t.Cleanup). Use existing generated mocks — never hand-write mocks.
No new mocks generated: All required mock types already exist in pkg/vmcp/aggregator/mocks/, pkg/vmcp/session/mocks/, and pkg/vmcp/client/mocks/; reuse them
pkg/vmcp/config/config.go — Config struct definition; needed to construct valid test configs
pkg/vmcp/aggregator/mocks/mock_interfaces.go — MockBackendDiscoverer and MockAggregator; import for mocking discoverer interactions
pkg/vmcp/session/mocks/mock_factory.go — MockMultiSessionFactory if session factory assertions are needed
pkg/vmcp/client/mocks/mock_outgoing_registry.go — MockOutgoingAuthRegistry for outgoing auth mocking
test/integration/vmcp/helpers/vmcp_server.go — Reference for how NewVMCPServer constructs and wires components; mirrors the wiring sequence in pkg/vmcp/cli/serve.go
.claude/rules/testing.md — Canonical test conventions: Ginkgo vs testify choice (testify for unit tests), gomock usage, parallel test safety, port allocation
Component Interfaces
The test files interact only with the exported API surface defined in #4879. Key signatures to target:
Note: If #4879 keeps these as unexported functions, test them from package cli (same package) rather than package cli_test. The choice should be driven by what #4879 ships.
Sample table-driven test skeleton for loadAndValidateConfig:
TestLoadAuthServerConfig_MalformedFile: write ::: into auth server file → returns error
TestDiscoverBackends_StaticMode: construct config.Config with non-empty Backends slice → expect backends returned equal to input count; no Kubernetes API needed
TestDiscoverBackends_EmptyGroupWarning: construct config.Config with empty Backends slice in non-Kubernetes environment → expect empty slice with no error (warns but does not fail)
TestCreateSessionFactory_WithHMACSecret: set VMCP_SESSION_HMAC_SECRET via t.Setenv → expect non-nil factory, nil error
TestCreateSessionFactory_NoSecret_NonKubernetes: no VMCP_SESSION_HMAC_SECRET, ensure KUBERNETES_SERVICE_HOST is unset → expect non-nil factory, nil error (development mode warning)
TestCreateSessionFactory_NoSecret_Kubernetes: set KUBERNETES_SERVICE_HOST via t.Setenv, clear VMCP_SESSION_HMAC_SECRET → expect nil factory, error containing "VMCP_SESSION_HMAC_SECRET environment variable is required"
TestCreateSessionFactory_ShortHMACSecret: set VMCP_SESSION_HMAC_SECRET to a value shorter than 32 bytes → expect non-nil factory, nil error (only a warning is logged; the factory is still created)
Auth server config file path is derived from filepath.Dir(configPath) — test with a config file that is in a nested subdirectory to confirm path joining is correct
VMCP_SESSION_HMAC_SECRET that is exactly 32 bytes long should produce no warning and a valid factory
Dynamic Kubernetes backend discovery path in discoverBackends (requires a live API server; covered by existing integration tests and future E2E tests in E2E tests: quick mode and config-file mode #4888)
Description
Add a comprehensive unit test suite in
pkg/vmcp/cli/for theServeandValidateentry points and their helper functions (loadAndValidateConfig,discoverBackends,createSessionFactory,loadAuthServerConfig). Tests usego.uber.org/mock(gomock) and write temporary config files to disk; no running server or Kubernetes cluster is required. This work was deliberately separated from #4879 to keep each PR within the 400-line / 10-file review limit.Context
#4879 creates
pkg/vmcp/cli/as a new library package, but intentionally defers thorough unit test coverage to keep its PR size manageable. This item delivers that coverage. Becausepkg/vmcp/cli/is business logic (not a thin CLI wrapper), organizational standards require thorough unit tests alongside the source files — the same principle that governs every other package underpkg/. The tests validate the config loading pipeline, validation rejection paths, backend discovery wiring, session factory HMAC-secret behavior (including the Kubernetes fail-fast rule), and the auth server config side-loading logic.Dependencies: Depends on #4879 (the
pkg/vmcp/cli/package must exist before tests can be written against it)Blocks: (none — can merge independently after #4879)
Acceptance Criteria
pkg/vmcp/cli/serve_test.goandpkg/vmcp/cli/validate_test.goexist, each carrying the required SPDX copyright headerloadAndValidateConfigunit tests cover: valid config round-trip, missing config file (error), malformed YAML (error), and config that fails semantic validation (error)loadAuthServerConfigunit tests cover: file absent (returns nil, nil), valid YAML file (returns parsed struct), and malformed YAML file (returns error)discoverBackendsunit tests cover: static mode (non-emptycfg.Backendsslice →NewUnifiedBackendDiscovererWithStaticBackendspath) and empty-backends warning pathcreateSessionFactoryunit tests cover: HMAC secret set via env var (factory created successfully), no secret + non-Kubernetes environment (factory created with warning), and no secret + Kubernetes environment (error returned)Validateunit tests cover: missing config path (error), valid config file (nil return), and invalid config (error)t.Cleanupfor teardown (notdefer) and random ports vianet.Listen("tcp", ":0")where port allocation is neededaggregator.Aggregator,aggregator.BackendDiscoverer, andvmcpauth.OutgoingAuthRegistryare sourced from the existing generated mocks inpkg/vmcp/aggregator/mocks/andpkg/vmcp/client/mocks/; no hand-written mocks are introducedgo test ./pkg/vmcp/cli/...passes with no failuresgo vet ./pkg/vmcp/cli/...passes with no issuesTechnical Approach
Recommended Implementation
Write two test files (
serve_test.goandvalidate_test.go) inpackage cli(same package as the source, allowing access to unexported helpers if needed) orpackage cli_test(external test package). Preferpackage cli_testunless a helper function is unexported and cannot be tested through the exported API — in that case usepackage clito keep coverage complete.For each function under test:
t.TempDir()path usingos.WriteFile.require.NoError/require.Error/require.ErrorContainsfromgithub.com/stretchr/testify/require.For
createSessionFactory, manipulate theVMCP_SESSION_HMAC_SECRETenvironment variable witht.Setenv(automatically restored after test). Mockruntime.IsKubernetesRuntime()behavior by settingKUBERNETES_SERVICE_HOSTenv var (the standard signal the container runtime package uses to detect Kubernetes context).For
discoverBackends, the dynamic Kubernetes discovery path insidediscoverBackendsrequires a live API server and is therefore not unit-testable. Focus unit tests on the static mode path (non-emptycfg.Backends) which is the relevant path for the local vMCP use case. The dynamic path is covered by existing integration tests intest/integration/vmcp/.Patterns & Frameworks
.gofile must open with// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.and// SPDX-License-Identifier: Apache-2.0github.com/stretchr/testify/requirefor fatal assertions (fail immediately),assertfor non-fatal checks in table-driven subtestsfor _, tc := range tests { t.Run(tc.name, func(t *testing.T) {...}) }for functions with multiple input variants (config loading, validation, HMAC secret scenarios)t.TempDir()for config files — automatically cleaned up after each testt.Setenv(key, value)— automatically restores the original value after the testt.Cleanup(func() {...})for any resources allocated inside tests, notdefernet.Listen("tcp", ":0")when a free port is needed; close the listener before passing the portctrl := gomock.NewController(t)(no manualctrl.Finish()needed — testify handles this automatically viat.Cleanup). Use existing generated mocks — never hand-write mocks.pkg/vmcp/aggregator/mocks/,pkg/vmcp/session/mocks/, andpkg/vmcp/client/mocks/; reuse themCode Pointers
pkg/vmcp/cli/serve.go(created by Extract shared vMCP logic intopkg/vmcp/cli/(serve + validate) #4879) — Primary file under test; containsServe,loadAndValidateConfig,discoverBackends,createSessionFactory,loadAuthServerConfig,getStatusReportingIntervalpkg/vmcp/cli/validate.go(created by Extract shared vMCP logic intopkg/vmcp/cli/(serve + validate) #4879) — Secondary file under test; containsValidateandValidateConfigpkg/vmcp/config/config.go—Configstruct definition; needed to construct valid test configspkg/vmcp/aggregator/mocks/mock_interfaces.go—MockBackendDiscovererandMockAggregator; import for mocking discoverer interactionspkg/vmcp/session/mocks/mock_factory.go—MockMultiSessionFactoryif session factory assertions are neededpkg/vmcp/client/mocks/mock_outgoing_registry.go—MockOutgoingAuthRegistryfor outgoing auth mockingtest/integration/vmcp/helpers/vmcp_server.go— Reference for howNewVMCPServerconstructs and wires components; mirrors the wiring sequence inpkg/vmcp/cli/serve.gocmd/vmcp/app/commands.go— Original source of all five functions; useful to understand the exact behavior being tested before Extract shared vMCP logic intopkg/vmcp/cli/(serve + validate) #4879 extracts them.claude/rules/testing.md— Canonical test conventions: Ginkgo vs testify choice (testify for unit tests), gomock usage, parallel test safety, port allocationComponent Interfaces
The test files interact only with the exported API surface defined in #4879. Key signatures to target:
Note: If #4879 keeps these as unexported functions, test them from
package cli(same package) rather thanpackage cli_test. The choice should be driven by what #4879 ships.Sample table-driven test skeleton for
loadAndValidateConfig:Testing Strategy
Unit Tests
TestValidate_MissingConfigPath: callValidatewith emptyConfigPath→ expect error containing "no configuration file specified"TestValidate_ValidConfig: write a minimal valid YAML to temp dir, callValidate→ expectnilerrorTestValidate_InvalidConfig: write YAML missing required fields (e.g. missinggroupRef), callValidate→ expect error containing "validation failed"TestLoadAndValidateConfig_Valid: valid YAML round-trip → returns non-nil*config.Configwith correct fields populatedTestLoadAndValidateConfig_MissingFile: non-existent path → error contains "configuration loading failed"TestLoadAndValidateConfig_MalformedYAML::::syntax error → error contains "configuration loading failed"TestLoadAndValidateConfig_FailsValidation: YAML that parses but has missing required fields → error contains "validation failed"TestLoadAuthServerConfig_Absent: no sibling file → returnsnil, nilTestLoadAuthServerConfig_ValidFile: write validauthserver-config.yaml→ returns populated*RunConfigTestLoadAuthServerConfig_MalformedFile: write:::into auth server file → returns errorTestDiscoverBackends_StaticMode: constructconfig.Configwith non-emptyBackendsslice → expect backends returned equal to input count; no Kubernetes API neededTestDiscoverBackends_EmptyGroupWarning: constructconfig.Configwith emptyBackendsslice in non-Kubernetes environment → expect empty slice with no error (warns but does not fail)TestCreateSessionFactory_WithHMACSecret: setVMCP_SESSION_HMAC_SECRETviat.Setenv→ expect non-nil factory, nil errorTestCreateSessionFactory_NoSecret_NonKubernetes: noVMCP_SESSION_HMAC_SECRET, ensureKUBERNETES_SERVICE_HOSTis unset → expect non-nil factory, nil error (development mode warning)TestCreateSessionFactory_NoSecret_Kubernetes: setKUBERNETES_SERVICE_HOSTviat.Setenv, clearVMCP_SESSION_HMAC_SECRET→ expect nil factory, error containing "VMCP_SESSION_HMAC_SECRET environment variable is required"TestCreateSessionFactory_ShortHMACSecret: setVMCP_SESSION_HMAC_SECRETto a value shorter than 32 bytes → expect non-nil factory, nil error (only a warning is logged; the factory is still created)Integration Tests
test/integration/vmcp/vmcp_integration_test.gocontinues to pass unchanged (no behavior regression from Extract shared vMCP logic intopkg/vmcp/cli/(serve + validate) #4879 extraction)Edge Cases
filepath.Dir(configPath)— test with a config file that is in a nested subdirectory to confirm path joining is correctVMCP_SESSION_HMAC_SECRETthat is exactly 32 bytes long should produce no warning and a valid factoryOut of Scope
Servefunction end-to-end (it blocks on a running HTTP server; end-to-end coverage belongs in E2E tests: quick mode and config-file mode #4888 E2E tests)discoverBackends(requires a live API server; covered by existing integration tests and future E2E tests in E2E tests: quick mode and config-file mode #4888)cmd/vmcp/app/commands.go(that is Thin-wrap standalone vmcp binary over pkg/vmcp/cli/ #4880)pkg/vmcp/cli/init.goorpkg/vmcp/cli/embedding_manager.go(those are Addinit.gotopkg/vmcp/cli/for config scaffolding #4882 and Implement EmbeddingServiceManager in pkg/vmcp/cli/ #4884 respectively)vmcpbinary behaviorpkg/vmcp/*/mocks/)References
pkg/vmcp/cli/(serve + validate) #4879 — Createspkg/vmcp/cli/serve.goandvalidate.gothat this item tests.claude/rules/testing.md— ToolHive test conventions (gomock, Ginkgo vs testify, port allocation,t.Cleanup).claude/rules/go-style.md— SPDX headers, error handling conventionstest/integration/vmcp/helpers/vmcp_server.go— Pattern reference for wiring vMCP components in tests