diff --git a/ai/metrics_test.go b/ai/metrics_test.go index 1e2de54..56d2d5d 100644 --- a/ai/metrics_test.go +++ b/ai/metrics_test.go @@ -156,6 +156,64 @@ func TestMetrics_ReadEvents_Good_FiltersBySince(t *testing.T) { } } +func TestMetrics_Record_Bad_DirectoryAsFile(t *testing.T) { + // If the home directory resolution fails, Record must return an error. + origHome := os.Getenv("HOME") + os.Setenv("HOME", "/dev/null/nonexistent") + t.Cleanup(func() { os.Setenv("HOME", origHome) }) + + event := Event{Type: "bad_test"} + err := Record(event) + if err == nil { + t.Error("expected error when home directory is invalid, got nil") + } +} + +func TestMetrics_ReadEvents_Bad_InvalidHome(t *testing.T) { + // ReadEvents must propagate the error when home resolution fails. + origHome := os.Getenv("HOME") + os.Setenv("HOME", "/dev/null/nonexistent") + t.Cleanup(func() { os.Setenv("HOME", origHome) }) + + _, err := ReadEvents(time.Now().Add(-time.Hour)) + if err == nil { + t.Error("expected error when home directory is invalid, got nil") + } +} + +func TestMetrics_Record_Ugly_ConcurrentWrites(t *testing.T) { + // Concurrent writes must not corrupt the JSONL file — all events must be readable. + withTempHome(t) + + const goroutines = 10 + const eventsPerGoroutine = 20 + + done := make(chan struct{}, goroutines) + for range goroutines { + go func() { + defer func() { done <- struct{}{} }() + for range eventsPerGoroutine { + if err := Record(Event{Type: "concurrent"}); err != nil { + t.Errorf("concurrent Record: %v", err) + return + } + } + }() + } + for range goroutines { + <-done + } + + events, err := ReadEvents(time.Now().Add(-time.Hour)) + if err != nil { + t.Fatalf("ReadEvents after concurrent writes: %v", err) + } + expected := goroutines * eventsPerGoroutine + if len(events) != expected { + t.Errorf("expected %d events after concurrent writes, got %d", expected, len(events)) + } +} + func TestMetrics_ReadMetricsFile_Good_MalformedLines(t *testing.T) { withTempHome(t) diff --git a/cmd/embed-bench/main.go b/cmd/embed-bench/main.go index 9b4f72b..3616e15 100644 --- a/cmd/embed-bench/main.go +++ b/cmd/embed-bench/main.go @@ -330,5 +330,7 @@ func truncate(text string, limit int) string { } func init() { - os.Stderr.Sync() + // Flush stderr before flag parsing so any buffered output is visible if the process is + // interrupted during startup (e.g. Ollama not running). + os.Stderr.Sync() //nolint:errcheck // sync on startup, error is not actionable } diff --git a/cmd/metrics/cmd.go b/cmd/metrics/cmd.go index 039709c..b344562 100644 --- a/cmd/metrics/cmd.go +++ b/cmd/metrics/cmd.go @@ -28,6 +28,9 @@ var metricsCmd = &cli.Command{ }, } +// initMetricsFlags binds the --since and --json flags to the metrics command. +// +// initMetricsFlags() // called once by AddMetricsCommand func initMetricsFlags() { metricsCmd.Flags().StringVar(&metricsSince, "since", "7d", i18n.T("cmd.ai.metrics.flag.since")) metricsCmd.Flags().BoolVar(&metricsJSON, "json", false, i18n.T("common.flag.json")) @@ -41,6 +44,10 @@ func AddMetricsCommand(parent *cli.Command) { parent.AddCommand(metricsCmd) } +// runMetrics reads and displays AI event metrics for the configured time window. +// +// // via CLI: core ai metrics --since 7d --json +// runMetrics() func runMetrics() error { sinceDuration, err := parseDuration(metricsSince) if err != nil { diff --git a/cmd/rag/cmd.go b/cmd/rag/cmd.go index ad27204..3d043d7 100644 --- a/cmd/rag/cmd.go +++ b/cmd/rag/cmd.go @@ -4,4 +4,6 @@ package rag import ragcmd "forge.lthn.ai/core/go-rag/cmd/rag" // AddRAGSubcommands registers RAG commands as subcommands of parent. +// +// rag.AddRAGSubcommands(aiCmd) // → core ai rag index|query|status var AddRAGSubcommands = ragcmd.AddRAGSubcommands diff --git a/cmd/security/cmd.go b/cmd/security/cmd.go index 3c8a4b4..88ff635 100644 --- a/cmd/security/cmd.go +++ b/cmd/security/cmd.go @@ -1 +1,6 @@ +// Package security implements the security command tree for the Core CLI. +// +// Commands registered: +// +// security.AddSecurityCommands(rootCmd) // → core security alerts|deps|scan|secrets|jobs package security diff --git a/cmd/security/cmd_alerts.go b/cmd/security/cmd_alerts.go index 14f999e..c3e5b7d 100644 --- a/cmd/security/cmd_alerts.go +++ b/cmd/security/cmd_alerts.go @@ -8,6 +8,9 @@ import ( "forge.lthn.ai/core/cli/pkg/cli" ) +// addAlertsCommand registers the alerts subcommand under parent. +// +// addAlertsCommand(securityCmd) // → core security alerts --repo core-php --severity high func addAlertsCommand(parent *cli.Command) { command := &cli.Command{ Use: "alerts", @@ -41,6 +44,10 @@ type AlertOutput struct { Message string `json:"message"` } +// runAlerts fetches and displays unified security alerts across all repos or a single target. +// +// // via CLI: core security alerts --repo core-php --severity high +// runAlerts() func runAlerts() error { if err := checkGH(); err != nil { return err diff --git a/cmd/security/cmd_deps.go b/cmd/security/cmd_deps.go index ee18d29..37f8911 100644 --- a/cmd/security/cmd_deps.go +++ b/cmd/security/cmd_deps.go @@ -8,6 +8,9 @@ import ( "forge.lthn.ai/core/cli/pkg/cli" ) +// addDepsCommand registers the deps subcommand under parent. +// +// addDepsCommand(securityCmd) // → core security deps --repo core-php --severity critical func addDepsCommand(parent *cli.Command) { command := &cli.Command{ Use: "deps", @@ -42,6 +45,10 @@ type DepAlert struct { Summary string `json:"summary"` } +// runDeps fetches and displays Dependabot vulnerability alerts across all repos or a single target. +// +// // via CLI: core security deps --repo core-php --severity critical +// runDeps() func runDeps() error { if err := checkGH(); err != nil { return err diff --git a/cmd/security/cmd_jobs.go b/cmd/security/cmd_jobs.go index 22fad9e..c839190 100644 --- a/cmd/security/cmd_jobs.go +++ b/cmd/security/cmd_jobs.go @@ -19,6 +19,9 @@ var ( jobsCopies int ) +// addJobsCommand registers the jobs subcommand under parent. +// +// addJobsCommand(securityCmd) // → core security jobs --targets wailsapp/wails --issue-repo host-uk/core func addJobsCommand(parent *cli.Command) { command := &cli.Command{ Use: "jobs", @@ -37,6 +40,10 @@ func addJobsCommand(parent *cli.Command) { parent.AddCommand(command) } +// runJobs aggregates security findings for each target and creates GitHub issues. +// +// // via CLI: core security jobs --targets wailsapp/wails,facebook/react --dry-run +// runJobs() func runJobs() error { if err := checkGH(); err != nil { return err diff --git a/cmd/security/cmd_scan.go b/cmd/security/cmd_scan.go index d636b75..bc81dd1 100644 --- a/cmd/security/cmd_scan.go +++ b/cmd/security/cmd_scan.go @@ -14,6 +14,9 @@ var ( scanTool string ) +// addScanCommand registers the scan subcommand under parent. +// +// addScanCommand(securityCmd) // → core security scan --repo core-php --tool gosec func addScanCommand(parent *cli.Command) { command := &cli.Command{ Use: "scan", @@ -48,6 +51,10 @@ type ScanAlert struct { Message string `json:"message"` } +// runScan fetches and displays code-scanning alerts across all repos or a single target. +// +// // via CLI: core security scan --repo core-php --tool gosec +// runScan() func runScan() error { if err := checkGH(); err != nil { return err diff --git a/cmd/security/cmd_secrets.go b/cmd/security/cmd_secrets.go index bd56977..ddac4c5 100644 --- a/cmd/security/cmd_secrets.go +++ b/cmd/security/cmd_secrets.go @@ -8,6 +8,9 @@ import ( "forge.lthn.ai/core/cli/pkg/cli" ) +// addSecretsCommand registers the secrets subcommand under parent. +// +// addSecretsCommand(securityCmd) // → core security secrets --repo core-php --json func addSecretsCommand(parent *cli.Command) { command := &cli.Command{ Use: "secrets", @@ -38,6 +41,10 @@ type SecretAlert struct { PushProtection bool `json:"push_protection_bypassed"` } +// runSecrets fetches and displays secret-scanning alerts across all repos or a single target. +// +// // via CLI: core security secrets --repo core-php +// runSecrets() func runSecrets() error { if err := checkGH(); err != nil { return err