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:
98
aggregator-agent/internal/scanner/windows_ghost_test.go
Normal file
98
aggregator-agent/internal/scanner/windows_ghost_test.go
Normal 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")
|
||||
}
|
||||
255
aggregator-agent/internal/scanner/windows_service_parity_test.go
Normal file
255
aggregator-agent/internal/scanner/windows_service_parity_test.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
59
aggregator-agent/internal/scanner/winget_logging_test.go
Normal file
59
aggregator-agent/internal/scanner/winget_logging_test.go
Normal 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.")
|
||||
}
|
||||
}
|
||||
158
aggregator-agent/internal/scanner/winget_parser_test.go
Normal file
158
aggregator-agent/internal/scanner/winget_parser_test.go
Normal 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")
|
||||
}
|
||||
77
aggregator-agent/internal/scanner/winget_path_test.go
Normal file
77
aggregator-agent/internal/scanner/winget_path_test.go
Normal 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
58
docs/C1_PreFix_Tests.md
Normal 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.
|
||||
Reference in New Issue
Block a user