test(windows): C-1 pre-fix tests for Windows-specific bugs

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) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 08:51:44 -04:00
parent 799c155d94
commit 38184a9625
6 changed files with 705 additions and 0 deletions

View File

@@ -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")
}

View File

@@ -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
}
}
}

View File

@@ -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.")
}
}

View File

@@ -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")
}

View File

@@ -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.")
}
}

58
docs/C1_PreFix_Tests.md Normal file
View File

@@ -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.