From 38184a9625b9c8df6d5fddcb895c31d8a8e3e83a Mon Sep 17 00:00:00 2001 From: jpetree331 Date: Sun, 29 Mar 2026 08:51:44 -0400 Subject: [PATCH] test(windows): C-1 pre-fix tests for Windows-specific bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-fix test suite for 7 Windows-specific findings. All tests are SHARED (no build tags) — they compile and run on Linux using source file inspection and direct function calls. Tests added: - F-C1-1 HIGH: Winget PATH-only search (2 tests) - F-C1-2 MEDIUM: Winget text parser spaces bug (4 tests) - F-C1-3 HIGH: Ghost updates — no post-install verification (3 tests) - F-C1-4 RESOLVED: Service auto-restart already configured (1 test) - F-C1-5 HIGH: Duplicated polling loop missing B-2 fixes (5 tests) - F-C1-6 LOW: Winget uses fmt.Printf (2 tests) - F-C1-7 LOW: Service has emojis in logs (2 tests) Current state: 8 FAIL, 11 PASS. All prior tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../internal/scanner/windows_ghost_test.go | 98 +++++++ .../scanner/windows_service_parity_test.go | 255 ++++++++++++++++++ .../internal/scanner/winget_logging_test.go | 59 ++++ .../internal/scanner/winget_parser_test.go | 158 +++++++++++ .../internal/scanner/winget_path_test.go | 77 ++++++ docs/C1_PreFix_Tests.md | 58 ++++ 6 files changed, 705 insertions(+) create mode 100644 aggregator-agent/internal/scanner/windows_ghost_test.go create mode 100644 aggregator-agent/internal/scanner/windows_service_parity_test.go create mode 100644 aggregator-agent/internal/scanner/winget_logging_test.go create mode 100644 aggregator-agent/internal/scanner/winget_parser_test.go create mode 100644 aggregator-agent/internal/scanner/winget_path_test.go create mode 100644 docs/C1_PreFix_Tests.md diff --git a/aggregator-agent/internal/scanner/windows_ghost_test.go b/aggregator-agent/internal/scanner/windows_ghost_test.go new file mode 100644 index 0000000..9223526 --- /dev/null +++ b/aggregator-agent/internal/scanner/windows_ghost_test.go @@ -0,0 +1,98 @@ +package scanner + +// windows_ghost_test.go — Pre-fix tests for ghost updates. +// [SHARED] — tests inspect installer source file which has no build tag. +// +// F-C1-3 HIGH: No post-install state verification for Windows Updates. +// +// Run: cd aggregator-agent && go test ./internal/scanner/... -v -run TestWindowsUpdate + +import ( + "os" + "strings" + "testing" +) + +// --------------------------------------------------------------------------- +// Test 3.1 — Documents missing post-install verification (F-C1-3) +// +// Category: PASS-NOW (documents the bug) +// --------------------------------------------------------------------------- + +func TestWindowsUpdateInstallerHasNoPostInstallVerification(t *testing.T) { + // F-C1-3 HIGH: After triggering installation, the agent does not + // verify IsInstalled=1 via the COM API. The next scan may return + // the update as still available (ghost update bug). + installerPath := "../installer/windows.go" + content, err := os.ReadFile(installerPath) + if err != nil { + t.Fatalf("failed to read installer/windows.go: %v", err) + } + + src := string(content) + + // Check for post-install verification patterns + hasPostVerify := strings.Contains(src, "IsInstalled") || + strings.Contains(src, "post_install_verify") || + strings.Contains(src, "verify_installed") || + strings.Contains(src, "reboot_pending") + + if hasPostVerify { + t.Error("[ERROR] [agent] [scanner] F-C1-3 already fixed: post-install verification found") + } + + t.Log("[INFO] [agent] [scanner] F-C1-3 confirmed: no post-install state verification") +} + +// --------------------------------------------------------------------------- +// Test 3.2 — Must verify post-install state (assert fix) +// +// Category: FAIL-NOW / PASS-AFTER-FIX +// --------------------------------------------------------------------------- + +func TestWindowsUpdateInstallerVerifiesPostInstallState(t *testing.T) { + // F-C1-3: After fix, verify IsInstalled=1 or filter reboot-pending. + installerPath := "../installer/windows.go" + content, err := os.ReadFile(installerPath) + if err != nil { + t.Fatalf("failed to read installer/windows.go: %v", err) + } + + src := string(content) + + hasPostVerify := strings.Contains(src, "IsInstalled") || + strings.Contains(src, "post_install") || + strings.Contains(src, "reboot_pending") || + strings.Contains(src, "verify_installed") + + if !hasPostVerify { + t.Errorf("[ERROR] [agent] [scanner] no post-install verification found.\n" + + "F-C1-3: must check IsInstalled or filter reboot-pending updates.") + } +} + +// --------------------------------------------------------------------------- +// Test 3.3 — Search criteria is correct (documentary) +// +// Category: PASS-NOW +// --------------------------------------------------------------------------- + +func TestWindowsUpdateSearchCriteriaExcludesInstalled(t *testing.T) { + // F-C1-3: The search criteria is correct in principle, + // but IsInstalled transitions asynchronously after install. + wuaPath := "windows_wua.go" + content, err := os.ReadFile(wuaPath) + if err != nil { + // On non-Windows, this file may not be compilable but exists on disk + t.Skipf("windows_wua.go not readable: %v (expected on non-Windows)", err) + return + } + + src := string(content) + + if !strings.Contains(src, `IsInstalled=0 AND IsHidden=0`) { + t.Error("[ERROR] [agent] [scanner] expected search criteria IsInstalled=0 AND IsHidden=0") + } + + t.Log("[INFO] [agent] [scanner] F-C1-3: search criteria is correct but not re-checked post-install") +} diff --git a/aggregator-agent/internal/scanner/windows_service_parity_test.go b/aggregator-agent/internal/scanner/windows_service_parity_test.go new file mode 100644 index 0000000..3e42406 --- /dev/null +++ b/aggregator-agent/internal/scanner/windows_service_parity_test.go @@ -0,0 +1,255 @@ +package scanner + +// windows_service_parity_test.go — Pre-fix tests for service polling loop parity. +// [SHARED] — reads service/windows.go as a text file, no Windows imports needed. +// +// F-C1-4 MEDIUM: Service has no auto-restart on crash. +// F-C1-5 HIGH: Service runAgent() is duplicated, missing B-2 fixes. +// F-C1-7 LOW: Service runAgent() uses emojis in logs. +// +// Run: cd aggregator-agent && go test ./internal/scanner/... -v -run TestWindowsService + +import ( + "os" + "strings" + "testing" +) + +// --------------------------------------------------------------------------- +// Test 4.1 — Documents missing FailureActions (F-C1-4) +// +// Category: PASS-NOW (documents the bug) +// --------------------------------------------------------------------------- + +func TestWindowsServiceHasAutoRestartOnCrash(t *testing.T) { + // F-C1-4 RESOLVED: Service DOES have RecoveryActions configured. + // The audit finding F-C1-4 was incorrect — SetRecoveryActions is called. + servicePath := "../service/windows.go" + content, err := os.ReadFile(servicePath) + if err != nil { + t.Skipf("service/windows.go not readable: %v", err) + return + } + + src := string(content) + + hasRecovery := strings.Contains(src, "RecoveryActions") || + strings.Contains(src, "FailureActions") + + if !hasRecovery { + t.Error("[ERROR] [agent] [service] no RecoveryActions configured") + } + + t.Log("[INFO] [agent] [service] F-C1-4 ALREADY CORRECT: service has crash recovery") +} + +// --------------------------------------------------------------------------- +// Test 5.1 — Documents fixed jitter in service (F-C1-5) +// +// Category: PASS-NOW (documents the bug) +// --------------------------------------------------------------------------- + +func TestWindowsServicePollingLoopHasFixedJitter(t *testing.T) { + // F-C1-5 HIGH: Windows service runAgent() is a duplicate of + // cmd/agent/main.go polling loop. B-2 fix (proportional jitter) + // was applied to main.go but NOT to service/windows.go. + servicePath := "../service/windows.go" + content, err := os.ReadFile(servicePath) + if err != nil { + t.Skipf("service/windows.go not readable: %v", err) + return + } + + src := string(content) + + // Find runAgent function + runIdx := strings.Index(src, "func (s *redflagService) runAgent()") + if runIdx == -1 { + t.Skip("[WARNING] [agent] [service] runAgent function not found") + return + } + + fnBody := src[runIdx:] + if len(fnBody) > 3000 { + fnBody = fnBody[:3000] + } + + // Check for OLD fixed jitter pattern + if strings.Contains(fnBody, "rand.Intn(30)") { + t.Log("[INFO] [agent] [service] F-C1-5 confirmed: service has old fixed 30s jitter") + } + + // Check that proportional jitter is NOT present + if strings.Contains(fnBody, "pollingInterval / 2") || strings.Contains(fnBody, "maxJitter") { + t.Error("[ERROR] [agent] [service] F-C1-5 already fixed: proportional jitter in service") + } +} + +// --------------------------------------------------------------------------- +// Test 5.2 — Service must have proportional jitter (assert fix) +// +// Category: FAIL-NOW / PASS-AFTER-FIX +// --------------------------------------------------------------------------- + +func TestWindowsServicePollingLoopHasProportionalJitter(t *testing.T) { + servicePath := "../service/windows.go" + content, err := os.ReadFile(servicePath) + if err != nil { + t.Skipf("service/windows.go not readable: %v", err) + return + } + + src := string(content) + + hasProportionalJitter := strings.Contains(src, "pollingInterval / 2") || + strings.Contains(src, "maxJitter") || + // Or the ideal fix: service delegates to shared function + strings.Contains(src, "runAgentLoop") || + strings.Contains(src, "commonPollingLoop") + + if !hasProportionalJitter { + t.Errorf("[ERROR] [agent] [service] service polling loop missing proportional jitter.\n" + + "F-C1-5: either deduplicate the loop or apply same jitter formula.") + } +} + +// --------------------------------------------------------------------------- +// Test 5.3 — Documents missing exponential backoff (F-C1-5) +// +// Category: PASS-NOW (documents the bug) +// --------------------------------------------------------------------------- + +func TestWindowsServicePollingLoopHasNoExponentialBackoff(t *testing.T) { + servicePath := "../service/windows.go" + content, err := os.ReadFile(servicePath) + if err != nil { + t.Skipf("service/windows.go not readable: %v", err) + return + } + + src := string(content) + + hasBackoff := strings.Contains(src, "calculateBackoff") || + strings.Contains(src, "consecutiveFailures") || + strings.Contains(src, "exponentialBackoff") + + if hasBackoff { + t.Error("[ERROR] [agent] [service] F-C1-5 already fixed: exponential backoff found in service") + } + + t.Log("[INFO] [agent] [service] F-C1-5 confirmed: no exponential backoff in service polling loop") +} + +// --------------------------------------------------------------------------- +// Test 5.4 — Service must have exponential backoff (assert fix) +// +// Category: FAIL-NOW / PASS-AFTER-FIX +// --------------------------------------------------------------------------- + +func TestWindowsServicePollingLoopHasExponentialBackoff(t *testing.T) { + servicePath := "../service/windows.go" + content, err := os.ReadFile(servicePath) + if err != nil { + t.Skipf("service/windows.go not readable: %v", err) + return + } + + src := string(content) + + hasBackoff := strings.Contains(src, "calculateBackoff") || + strings.Contains(src, "consecutiveFailures") || + strings.Contains(src, "backoffDelay") || + // Or the ideal fix: delegates to shared function + strings.Contains(src, "runAgentLoop") + + if !hasBackoff { + t.Errorf("[ERROR] [agent] [service] service missing exponential backoff.\n" + + "F-C1-5: apply same backoff or deduplicate polling loop.") + } +} + +// --------------------------------------------------------------------------- +// Test 5.5 — Polling loop should NOT be duplicated (assert fix) +// +// Category: FAIL-NOW / PASS-AFTER-FIX +// --------------------------------------------------------------------------- + +func TestPollingLoopIsNotDuplicated(t *testing.T) { + // F-C1-5: The ideal fix is to extract the polling loop into + // a shared function that both main.go and service/windows.go call. + servicePath := "../service/windows.go" + content, err := os.ReadFile(servicePath) + if err != nil { + t.Skipf("service/windows.go not readable: %v", err) + return + } + + src := string(content) + + // Check if service has its own GetCommands loop (duplication) + hasOwnLoop := strings.Contains(src, "GetCommands") && + strings.Contains(src, "for {") && + strings.Contains(src, "time.Sleep") + + if hasOwnLoop { + t.Errorf("[ERROR] [agent] [service] service has its own polling loop.\n" + + "F-C1-5: extract polling into shared function to prevent divergence.") + } +} + +// --------------------------------------------------------------------------- +// Test 6.3 — Documents emoji in service logs (F-C1-7) +// +// Category: PASS-NOW (documents the bug) +// --------------------------------------------------------------------------- + +func TestWindowsServiceHasEmojiInLogs(t *testing.T) { + // F-C1-7 LOW: Windows service runAgent() uses emojis in log messages. + servicePath := "../service/windows.go" + content, err := os.ReadFile(servicePath) + if err != nil { + t.Skipf("service/windows.go not readable: %v", err) + return + } + + src := string(content) + + hasEmoji := false + for _, r := range src { + if r >= 0x1F300 || (r >= 0x2600 && r <= 0x27BF) { + hasEmoji = true + break + } + } + + if !hasEmoji { + t.Error("[ERROR] [agent] [service] F-C1-7 already fixed: no emojis found in service code") + } + + t.Log("[INFO] [agent] [service] F-C1-7 confirmed: emojis in service log messages") +} + +// --------------------------------------------------------------------------- +// Test 6.4 — Service must have no emojis (assert fix) +// +// Category: FAIL-NOW / PASS-AFTER-FIX +// --------------------------------------------------------------------------- + +func TestWindowsServiceHasNoEmojiInLogs(t *testing.T) { + servicePath := "../service/windows.go" + content, err := os.ReadFile(servicePath) + if err != nil { + t.Skipf("service/windows.go not readable: %v", err) + return + } + + src := string(content) + + for _, r := range src { + if r >= 0x1F300 || (r >= 0x2600 && r <= 0x27BF) { + t.Errorf("[ERROR] [agent] [service] emoji found in service code (U+%04X).\n"+ + "F-C1-7: ETHOS #1 prohibits emojis in logs.", r) + return + } + } +} diff --git a/aggregator-agent/internal/scanner/winget_logging_test.go b/aggregator-agent/internal/scanner/winget_logging_test.go new file mode 100644 index 0000000..4b2c7e5 --- /dev/null +++ b/aggregator-agent/internal/scanner/winget_logging_test.go @@ -0,0 +1,59 @@ +package scanner + +// winget_logging_test.go — Pre-fix tests for winget logging format. +// [SHARED] — no build tag, compiles on all platforms. +// +// F-C1-6 LOW: Winget scanner uses fmt.Printf not structured logging. +// +// Run: cd aggregator-agent && go test ./internal/scanner/... -v -run TestWingetScanner + +import ( + "os" + "strings" + "testing" +) + +// --------------------------------------------------------------------------- +// Test 6.1 — Documents unstructured logging (F-C1-6) +// +// Category: PASS-NOW (documents the bug) +// --------------------------------------------------------------------------- + +func TestWingetScannerUsesStructuredLogging(t *testing.T) { + // F-C1-6 LOW: winget scanner uses fmt.Printf for error output. + // ETHOS #1 requires [TAG] [system] [component] format via log.Printf. + content, err := os.ReadFile("winget.go") + if err != nil { + t.Fatalf("failed to read winget.go: %v", err) + } + + src := string(content) + + hasFmtPrintf := strings.Contains(src, "fmt.Printf") + + if !hasFmtPrintf { + t.Error("[ERROR] [agent] [scanner] F-C1-6 already fixed: no fmt.Printf in winget.go") + } + + t.Log("[INFO] [agent] [scanner] F-C1-6 confirmed: fmt.Printf used instead of log.Printf") +} + +// --------------------------------------------------------------------------- +// Test 6.2 — Must have no fmt.Printf (assert fix) +// +// Category: FAIL-NOW / PASS-AFTER-FIX +// --------------------------------------------------------------------------- + +func TestWingetScannerHasNoFmtPrintf(t *testing.T) { + content, err := os.ReadFile("winget.go") + if err != nil { + t.Fatalf("failed to read winget.go: %v", err) + } + + src := string(content) + + if strings.Contains(src, "fmt.Printf") { + t.Errorf("[ERROR] [agent] [scanner] winget.go contains fmt.Printf.\n" + + "F-C1-6: all output must use log.Printf with ETHOS format.") + } +} diff --git a/aggregator-agent/internal/scanner/winget_parser_test.go b/aggregator-agent/internal/scanner/winget_parser_test.go new file mode 100644 index 0000000..a606c7a --- /dev/null +++ b/aggregator-agent/internal/scanner/winget_parser_test.go @@ -0,0 +1,158 @@ +package scanner + +// winget_parser_test.go — Pre-fix tests for winget output parsing. +// [SHARED] — no build tag, compiles on all platforms. +// +// F-C1-2 MEDIUM: Text fallback parser splits on whitespace, +// breaks on package names with spaces. +// F-C1-8 LOW: No winget parsing tests existed before this file. +// +// Run: cd aggregator-agent && go test ./internal/scanner/... -v -run TestWinget + +import ( + "encoding/json" + "testing" +) + +// --------------------------------------------------------------------------- +// Test 2.1 — Text parser handles spaces in package names (assert fix) +// +// Category: FAIL-NOW / PASS-AFTER-FIX +// --------------------------------------------------------------------------- + +func TestWingetTextParserHandlesSpacesInPackageNames(t *testing.T) { + // F-C1-2 MEDIUM: Text parser splits on all whitespace. + // Package names with spaces are truncated to the first word. + // After fix: use column-position parsing based on header separator line. + scanner := NewWingetScanner() + + mockOutput := "Name Id Version Available\n" + + "------------------------------------------------------------------------------------\n" + + "Microsoft Visual Studio Code Microsoft.VisualStudioCode 1.85.0 1.86.0\n" + + "Git Git.Git 2.43.0 2.44.0\n" + + updates, err := scanner.parseWingetTextOutput(mockOutput) + if err != nil { + t.Fatalf("parse error: %v", err) + } + + if len(updates) == 0 { + t.Fatal("[ERROR] [agent] [scanner] no updates parsed from text output") + } + + // The first package should have the full name + firstName := updates[0].PackageName + if firstName != "Microsoft Visual Studio Code" { + t.Errorf("[ERROR] [agent] [scanner] expected full name \"Microsoft Visual Studio Code\", got %q.\n"+ + "F-C1-2: text parser truncates names at first space.", firstName) + } +} + +// --------------------------------------------------------------------------- +// Test 2.2 — Documents text parser truncation (F-C1-2) +// +// Category: PASS-NOW (documents the bug) +// --------------------------------------------------------------------------- + +func TestWingetTextParserCurrentlyBreaksOnSpaces(t *testing.T) { + // F-C1-2: Documents that the current parser truncates + // package names at the first space. + scanner := NewWingetScanner() + + mockOutput := "Name Id Version Available\n" + + "------------------------------------------------------------------------------------\n" + + "Microsoft Visual Studio Code Microsoft.VisualStudioCode 1.85.0 1.86.0\n" + + updates, err := scanner.parseWingetTextOutput(mockOutput) + if err != nil { + t.Fatalf("parse error: %v", err) + } + + if len(updates) == 0 { + t.Fatal("[ERROR] [agent] [scanner] no updates parsed") + } + + // Bug: first word only is captured + if updates[0].PackageName == "Microsoft Visual Studio Code" { + t.Error("[ERROR] [agent] [scanner] F-C1-2 already fixed: full name parsed correctly") + } + + t.Logf("[INFO] [agent] [scanner] F-C1-2 confirmed: first package name = %q (truncated)", updates[0].PackageName) +} + +// --------------------------------------------------------------------------- +// Test 2.3 — JSON parser handles spaces correctly +// +// Category: PASS-NOW (JSON path works) +// --------------------------------------------------------------------------- + +func TestWingetJsonParserHandlesSpacesInPackageNames(t *testing.T) { + // F-C1-2: JSON parser handles spaces correctly. + // The text fallback is the broken path. + scanner := NewWingetScanner() + + // Mock JSON output matching WingetPackage struct + mockJSON := `[ + {"Name":"Microsoft Visual Studio Code","Id":"Microsoft.VisualStudioCode","Version":"1.85.0","Available":"1.86.0","Source":"winget"}, + {"Name":"Git","Id":"Git.Git","Version":"2.43.0","Available":"2.44.0","Source":"winget"} + ]` + + var packages []WingetPackage + if err := json.Unmarshal([]byte(mockJSON), &packages); err != nil { + t.Fatalf("JSON parse error: %v", err) + } + + if len(packages) < 1 { + t.Fatal("[ERROR] [agent] [scanner] no packages parsed from JSON") + } + + // JSON parser should get full name + item := scanner.parseWingetPackage(packages[0]) + if item.PackageName != "Microsoft Visual Studio Code" { + t.Errorf("[ERROR] [agent] [scanner] expected full name, got %q", item.PackageName) + } + + t.Log("[INFO] [agent] [scanner] F-C1-2: JSON parser handles spaces correctly") +} + +// --------------------------------------------------------------------------- +// Test 2.4 — Both parsers return consistent structure +// +// Category: PASS-NOW +// --------------------------------------------------------------------------- + +func TestWingetParserReturnsConsistentStructure(t *testing.T) { + // F-C1-2: Both parsers must return the same struct type. + scanner := NewWingetScanner() + + // JSON path + pkg := WingetPackage{ + Name: "Git", + ID: "Git.Git", + Version: "2.43.0", + Available: "2.44.0", + Source: "winget", + } + jsonResult := scanner.parseWingetPackage(pkg) + + // Text path (simple name without spaces) + textOutput := "Name Id Version Available\n" + + "----------------------------------\n" + + "Git Git.Git 2.43.0 2.44.0\n" + textResults, err := scanner.parseWingetTextOutput(textOutput) + if err != nil { + t.Fatalf("text parse error: %v", err) + } + + if len(textResults) == 0 { + t.Fatal("[ERROR] [agent] [scanner] no results from text parser") + } + + // Both should have the same package type + if jsonResult.PackageType != textResults[0].PackageType { + t.Errorf("[ERROR] [agent] [scanner] package type mismatch: json=%q text=%q", + jsonResult.PackageType, textResults[0].PackageType) + } + + t.Log("[INFO] [agent] [scanner] F-C1-2: both parsers return consistent structure") +} diff --git a/aggregator-agent/internal/scanner/winget_path_test.go b/aggregator-agent/internal/scanner/winget_path_test.go new file mode 100644 index 0000000..95bb538 --- /dev/null +++ b/aggregator-agent/internal/scanner/winget_path_test.go @@ -0,0 +1,77 @@ +package scanner + +// winget_path_test.go — Pre-fix tests for winget path detection. +// [SHARED] — no build tag, compiles on all platforms. +// +// F-C1-1 HIGH: Winget located via exec.LookPath (PATH only). +// When running as SYSTEM service, winget is not in PATH. +// +// Run: cd aggregator-agent && go test ./internal/scanner/... -v -run TestWinget + +import ( + "os" + "strings" + "testing" +) + +// --------------------------------------------------------------------------- +// Test 1.1 — Documents PATH-only winget search (F-C1-1) +// +// Category: PASS-NOW (documents the bug) +// --------------------------------------------------------------------------- + +func TestWingetSearchesPathOnly(t *testing.T) { + // F-C1-1 HIGH: Winget located via PATH only. Windows service + // runs as SYSTEM which does not have %LOCALAPPDATA% in PATH. + // Per-user winget installs are invisible to the SYSTEM account. + content, err := os.ReadFile("winget.go") + if err != nil { + t.Fatalf("failed to read winget.go: %v", err) + } + + src := string(content) + + // Uses exec.LookPath for discovery + if !strings.Contains(src, `exec.LookPath("winget")`) { + t.Error("[ERROR] [agent] [scanner] expected exec.LookPath in winget discovery") + } + + // Does NOT check known system-wide install paths + hasKnownPaths := strings.Contains(src, "WindowsApps") || + strings.Contains(src, "LOCALAPPDATA") || + strings.Contains(src, "ProgramFiles") || + strings.Contains(src, `winget.exe`) + + if hasKnownPaths { + t.Error("[ERROR] [agent] [scanner] F-C1-1 already fixed: known winget paths checked") + } + + t.Log("[INFO] [agent] [scanner] F-C1-1 confirmed: winget uses PATH-only search") +} + +// --------------------------------------------------------------------------- +// Test 1.2 — Winget must check known install locations (assert fix) +// +// Category: FAIL-NOW / PASS-AFTER-FIX +// --------------------------------------------------------------------------- + +func TestWingetChecksKnownInstallLocations(t *testing.T) { + // F-C1-1: After fix, winget discovery must check known + // system-wide install paths in addition to PATH search. + content, err := os.ReadFile("winget.go") + if err != nil { + t.Fatalf("failed to read winget.go: %v", err) + } + + src := string(content) + + hasKnownPaths := strings.Contains(src, "WindowsApps") || + strings.Contains(src, "LOCALAPPDATA") || + strings.Contains(src, "ProgramFiles") || + strings.Contains(src, `winget.exe`) + + if !hasKnownPaths { + t.Errorf("[ERROR] [agent] [scanner] winget does not check known install locations.\n" + + "F-C1-1: must check known paths for SYSTEM service compatibility.") + } +} diff --git a/docs/C1_PreFix_Tests.md b/docs/C1_PreFix_Tests.md new file mode 100644 index 0000000..14b2a55 --- /dev/null +++ b/docs/C1_PreFix_Tests.md @@ -0,0 +1,58 @@ +# C-1 Pre-Fix Test Suite + +**Date:** 2026-03-29 +**Branch:** culurien +**Purpose:** Document Windows-specific bugs BEFORE fixes. +**Reference:** docs/C1_Windows_Audit.md + +--- + +## Test Files + +| File | Package | Tag | Bugs | +|------|---------|-----|------| +| `scanner/winget_path_test.go` | `scanner` | SHARED | F-C1-1 | +| `scanner/winget_parser_test.go` | `scanner` | SHARED | F-C1-2, F-C1-8 | +| `scanner/winget_logging_test.go` | `scanner` | SHARED | F-C1-6 | +| `scanner/windows_ghost_test.go` | `scanner` | SHARED | F-C1-3 | +| `scanner/windows_service_parity_test.go` | `scanner` | SHARED | F-C1-4, F-C1-5, F-C1-7 | + +All tests read source files as text — no Windows APIs needed. +All compile and run on Linux. Zero platform-specific imports. + +--- + +## State-Change Summary + +| Test | Bug | Current | After Fix | +|------|-----|---------|-----------| +| TestWingetSearchesPathOnly | F-C1-1 | PASS | update | +| TestWingetChecksKnownInstallLocations | F-C1-1 | **FAIL** | PASS | +| TestWingetTextParserHandlesSpacesInPackageNames | F-C1-2 | **FAIL** | PASS | +| TestWingetTextParserCurrentlyBreaksOnSpaces | F-C1-2 | PASS | update | +| TestWingetJsonParserHandlesSpacesInPackageNames | F-C1-2 | PASS | PASS | +| TestWingetParserReturnsConsistentStructure | F-C1-2 | PASS | PASS | +| TestWindowsUpdateInstallerHasNoPostInstallVerification | F-C1-3 | PASS | update | +| TestWindowsUpdateInstallerVerifiesPostInstallState | F-C1-3 | **FAIL** | PASS | +| TestWindowsUpdateSearchCriteriaExcludesInstalled | F-C1-3 | PASS | PASS | +| TestWindowsServiceHasAutoRestartOnCrash | F-C1-4 | PASS | PASS | +| TestWindowsServicePollingLoopHasFixedJitter | F-C1-5 | PASS | update | +| TestWindowsServicePollingLoopHasProportionalJitter | F-C1-5 | **FAIL** | PASS | +| TestWindowsServicePollingLoopHasNoExponentialBackoff | F-C1-5 | PASS | update | +| TestWindowsServicePollingLoopHasExponentialBackoff | F-C1-5 | **FAIL** | PASS | +| TestPollingLoopIsNotDuplicated | F-C1-5 | **FAIL** | PASS | +| TestWingetScannerUsesStructuredLogging | F-C1-6 | PASS | update | +| TestWingetScannerHasNoFmtPrintf | F-C1-6 | **FAIL** | PASS | +| TestWindowsServiceHasEmojiInLogs | F-C1-7 | PASS | update | +| TestWindowsServiceHasNoEmojiInLogs | F-C1-7 | **FAIL** | PASS | + +**8 FAIL** (assert post-fix), **11 PASS** (document state). + +--- + +## Notes + +1. F-C1-4 was resolved during testing: `SetRecoveryActions` already exists in the service code. The audit finding was incorrect. +2. All tests are SHARED (no build tags) — they read source files as text. +3. Winget parser tests (Part 2) call Go functions directly — they test pure parsing logic. +4. All prior B-2 and A-series agent tests continue to pass.