package service import ( "context" "log" "time" "scm.bstein.dev/bstein/ananke/internal/config" "scm.bstein.dev/bstein/ananke/internal/metrics" ) type TestHookGitOpsRunner func(context.Context, string, ...string) ([]byte, error) // TestHookParseGitRepositories exposes GitRepository parsing to the top-level // testing module. // Signature: TestHookParseGitRepositories(raw []byte) ([]metrics.GitOpsFluxSource, error). // Why: Ananke keeps split-module tests outside internal packages, but the // parser needs direct contract coverage without a live cluster. func TestHookParseGitRepositories(raw []byte) ([]metrics.GitOpsFluxSource, error) { return parseGitRepositories(raw) } // TestHookParseKustomizations exposes Kustomization parsing to the top-level // testing module. // Signature: TestHookParseKustomizations(raw []byte) ([]metrics.GitOpsKustomization, error). // Why: dashboard-facing labels should be verified from representative Flux JSON // without relying on the current production cluster shape. func TestHookParseKustomizations(raw []byte) ([]metrics.GitOpsKustomization, error) { return parseKustomizations(raw) } // TestHookParseHelmReleases exposes HelmRelease parsing to the top-level // testing module. // Signature: TestHookParseHelmReleases(raw []byte) ([]metrics.GitOpsHelmRelease, error). // Why: Helm status fields vary by reconciliation phase, and split-module tests // need a stable seam for parser coverage. func TestHookParseHelmReleases(raw []byte) ([]metrics.GitOpsHelmRelease, error) { return parseHelmReleases(raw) } // TestHookCollectGitOpsSnapshotWithRunner runs snapshot collection with an // injected kubectl runner. // Signature: TestHookCollectGitOpsSnapshotWithRunner(ctx context.Context, cfg config.Config, runner TestHookGitOpsRunner) metrics.GitOpsSnapshot. // Why: this covers success and partial-failure collection paths without shelling // out to kubectl from unit tests. func TestHookCollectGitOpsSnapshotWithRunner(ctx context.Context, cfg config.Config, runner TestHookGitOpsRunner) metrics.GitOpsSnapshot { original := gitOpsKubectlOutput defer func() { gitOpsKubectlOutput = original }() gitOpsKubectlOutput = runner return collectGitOpsSnapshot(ctx, cfg) } // TestHookMaybeStartGitOpsSnapshot starts the daemon's background GitOps scrape // from split-module tests. // Signature: TestHookMaybeStartGitOpsSnapshot(ctx context.Context, cfg config.Config, exporter *metrics.Exporter, logger *log.Logger, lastRun *time.Time, running bool, done chan<- struct{}) bool. // Why: the daemon intentionally keeps scraping asynchronous so GitOps telemetry // cannot delay UPS polling; the behavior needs direct coverage. func TestHookMaybeStartGitOpsSnapshot(ctx context.Context, cfg config.Config, exporter *metrics.Exporter, logger *log.Logger, lastRun *time.Time, running bool, done chan<- struct{}) bool { d := &Daemon{cfg: cfg, exporter: exporter, log: logger} return d.maybeStartGitOpsSnapshot(ctx, lastRun, running, done) } // TestHookMaybeStartGitOpsSnapshotWithRunner starts a background GitOps scrape // with an injected runner and restores the production runner after completion. // Signature: TestHookMaybeStartGitOpsSnapshotWithRunner(ctx context.Context, cfg config.Config, exporter *metrics.Exporter, logger *log.Logger, lastRun *time.Time, running bool, done chan<- struct{}, runner TestHookGitOpsRunner) bool. // Why: the scrape is asynchronous, so split-module tests need a seam that keeps // fake kubectl behavior installed until the goroutine exits. func TestHookMaybeStartGitOpsSnapshotWithRunner(ctx context.Context, cfg config.Config, exporter *metrics.Exporter, logger *log.Logger, lastRun *time.Time, running bool, done chan<- struct{}, runner TestHookGitOpsRunner) bool { original := gitOpsKubectlOutput gitOpsKubectlOutput = runner proxyDone := make(chan struct{}, 1) d := &Daemon{cfg: cfg, exporter: exporter, log: logger} started := d.maybeStartGitOpsSnapshot(ctx, lastRun, running, proxyDone) if !started || running { gitOpsKubectlOutput = original return started } go func() { <-proxyDone gitOpsKubectlOutput = original select { case done <- struct{}{}: default: } }() return started } // TestHookRunGitOpsKubectlOutput exposes the production command runner to // split-module tests. // Signature: TestHookRunGitOpsKubectlOutput(ctx context.Context, name string, args ...string) ([]byte, error). // Why: stderr preservation and empty-stderr failure behavior are part of the // operator-facing diagnostics contract. func TestHookRunGitOpsKubectlOutput(ctx context.Context, name string, args ...string) ([]byte, error) { return runGitOpsKubectlOutput(ctx, name, args...) } // TestHookGitOpsDefaultString exposes defaultString to split-module tests. // Signature: TestHookGitOpsDefaultString(value string, fallback string) string. // Why: fallback label behavior is intentionally tiny but important for readable // Grafana tables during startup. func TestHookGitOpsDefaultString(value string, fallback string) string { return defaultString(value, fallback) } // TestHookGitOpsFirstNonEmpty exposes firstNonEmpty to split-module tests. // Signature: TestHookGitOpsFirstNonEmpty(values ...string) string. // Why: revision fallback order should remain deterministic as Flux status // payloads change. func TestHookGitOpsFirstNonEmpty(values ...string) string { return firstNonEmpty(values...) } // TestHookGitOpsErrorSummary exposes gitOpsErrorSummary to split-module tests. // Signature: TestHookGitOpsErrorSummary(errors map[string]string) string. // Why: sorted scrape warnings are easier to inspect in journald during recovery. func TestHookGitOpsErrorSummary(errors map[string]string) string { return gitOpsErrorSummary(errors) } // TestHookFluxReadyMissing exposes the missing Ready-condition branch to tests. // Signature: TestHookFluxReadyMissing() (bool, string). // Why: absent Ready conditions should be treated as not-ready and labelled // explicitly instead of being mistaken for healthy. func TestHookFluxReadyMissing() (bool, string) { return fluxReady(nil) }