diff --git a/README.md b/README.md
index a0d4459..c7e3999 100644
--- a/README.md
+++ b/README.md
@@ -809,6 +809,16 @@ AetherForge is optimized for large-scale fleet management:
---
+## Testing & Validation Setup
+
+AetherForge includes a full cross-platform test suite (`test.bat` or `scripts/test-suite.ps1`) covering Go backend services, Go agent modules, Fusion bundler, and Vite/React frontend components:
+
+- **Full Suite Execution:** Run `.\test.bat` from PowerShell / CMD to validate all unit, compilation, and E2E test phases.
+- **Fast Unit Testing:** Run `.\test.bat -SkipE2E -SkipBuild` to execute all Go and Vitest unit tests in seconds without waiting for production binary builds or Playwright browser runs.
+- **Offline Network Isolation:** `recon` module unit tests utilize stubbed banner hooks (`SetBannerHooks`, `SetPortDialHook`, `SetFetchPageHook`) to isolate tests from real DNS resolutions and external HTTP requests.
+
+---
+
## License
Private use. Monero mining uses the RandomX algorithm (BSD-3-Clause) via `git.gammaspectra.live/P2Pool/go-randomx`.
diff --git a/server/internal/recon/recon_stream_test.go b/server/internal/recon/recon_stream_test.go
index 389d901..e1ee860 100644
--- a/server/internal/recon/recon_stream_test.go
+++ b/server/internal/recon/recon_stream_test.go
@@ -29,6 +29,13 @@ func TestScanStreamEmitsPortsFirst(t *testing.T) {
return 200, `
Home`, nil
})
t.Cleanup(func() { SetFetchPageHook(nil) })
+ SetBannerHooks(
+ func(_ string, p int) string { if p == 22 { return "SSH" }; return "" },
+ func(_ string, p int) (string, string) { if p == 80 { return "t", "s" }; return "", "" },
+ func(_ string, p int) string { if p == 5985 { return "w" }; return "" },
+ nil,
+ )
+ t.Cleanup(func() { SetBannerHooks(nil, nil, nil, nil) })
var events []string
report, err := ScanStream(ScanRequest{Host: "stream.lab", Profile: ProfileQuick, Port: 80, Scheme: "http"}, "scan-1", func(eventType string, _ map[string]interface{}) {
diff --git a/server/internal/recon/recon_test.go b/server/internal/recon/recon_test.go
index 3ac09e1..b92fbb0 100644
--- a/server/internal/recon/recon_test.go
+++ b/server/internal/recon/recon_test.go
@@ -143,6 +143,13 @@ func TestScanOwnedTarget(t *testing.T) {
return 200, ``, nil
})
t.Cleanup(func() { SetFetchPageHook(nil) })
+ SetBannerHooks(
+ func(_ string, p int) string { if p == 22 { return "SSH" }; return "" },
+ func(_ string, p int) (string, string) { if p == 80 { return "t", "s" }; return "", "" },
+ func(_ string, p int) string { if p == 5985 { return "w" }; return "" },
+ nil,
+ )
+ t.Cleanup(func() { SetBannerHooks(nil, nil, nil, nil) })
report, err := Scan(ScanRequest{Host: "owned.lab", Port: 80, Scheme: "http"})
if err != nil {
t.Fatal(err)
@@ -261,6 +268,13 @@ func TestScanReportIncludesAdminSurfaceJSON(t *testing.T) {
return resp.StatusCode, body, nil
})
t.Cleanup(func() { SetFetchPageHook(nil) })
+ SetBannerHooks(
+ func(_ string, p int) string { if p == 22 { return "SSH" }; return "" },
+ func(_ string, p int) (string, string) { if p == 80 { return "t", "s" }; return "", "" },
+ func(_ string, p int) string { if p == 5985 { return "w" }; return "" },
+ nil,
+ )
+ t.Cleanup(func() { SetBannerHooks(nil, nil, nil, nil) })
report, err := Scan(ScanRequest{Host: u.Hostname(), Port: port, Scheme: u.Scheme})
if err != nil {
t.Fatal(err)