Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
02fed8f
docs(agent): design Windows lifecycle durability
Jul 14, 2026
3ae6d27
docs(agent): plan Windows lifecycle durability
Jul 14, 2026
89d72d5
refactor(agent): share Windows helper eligibility rules
Jul 14, 2026
67f9171
fix(agent): scope helper admission to Windows sessions
Jul 14, 2026
64e40af
fix(agent): preserve role-aware helper admission
Jul 14, 2026
935a719
fix(agent): authenticate user helpers in matching RDP sessions
Jul 14, 2026
d413bde
fix(agent): own helper processes by session and role
Jul 14, 2026
8e9b819
fix(agent): close helper lifecycle ownership races
Jul 14, 2026
7a7f029
fix(agent): close lifecycle handoff races
Jul 14, 2026
8c81181
fix(agent): retain system helper on RDP disconnect
Jul 14, 2026
3fe734f
fix(agent): contain Windows helpers in a kill-on-close job
Jul 14, 2026
a2ca184
fix(agent): retain helper job until confirmed exit
Jul 14, 2026
0e6d4f0
test(agent): cover RDS helper convergence on Windows
Jul 14, 2026
2fee77a
feat(agent): classify process roles and helper fallback
Jul 14, 2026
bbec3d3
fix(agent): warn before helper process creation
Jul 14, 2026
09a495f
fix(agent): enforce one full Windows agent instance
Jul 15, 2026
692864c
fix(agent): acquire Windows instance guard before startup
Jul 15, 2026
920b59d
feat(agent): report Windows process roles and fallback mode
Jul 15, 2026
eb2b4f3
chore(agent): remove internal task report
Jul 15, 2026
01e1afb
docs(agent): plan helper lifecycle review remediation
Jul 15, 2026
77c75f5
test(agent): restore deleted helper role-authorization gate cases
Jul 15, 2026
df48d08
ci(agent): auto-discover Windows test packages
Jul 15, 2026
2d46974
fix(agent): refuse helper spawn for non-lifecycle roles
Jul 15, 2026
3529c8d
fix(agent): make helper liveness express unknown and fail closed
Jul 15, 2026
e7560cc
fix(agent): recycle helpers that never reach IPC
Jul 15, 2026
f2ecbb1
fix(agent): surface failed helper termination at warn level
Jul 15, 2026
be281d2
fix(agent): retry helper lifecycle bootstrap before listening
Jul 15, 2026
0785194
fix(agent): stop recording fabricated exit codes for detached helpers
Jul 15, 2026
74aa015
docs(agent): mark helper lifecycle remediation phase 1 complete
Jul 15, 2026
bd81a28
refactor(watchdog): return structured recovery outcomes
Jul 15, 2026
66c3bd6
docs(agent): correct helper startup timeout rationale
Jul 15, 2026
3cb3837
test(agent): let Windows pipe tests run on non-interactive logons
Jul 15, 2026
d35ab35
docs(agent): record real-Windows verification results
Jul 15, 2026
573687b
docs(agent): remove internal hostname from verification notes
Jul 15, 2026
8befc57
fix(agent): restore user helper on RDP and console reconnect
Jul 15, 2026
01769f2
fix(watchdog): verify Windows service transitions
Jul 15, 2026
6a32b52
fix(watchdog): verify forced recovery identity and ordering
Jul 15, 2026
b2da330
test(agent): skip SYSTEM-only pipe tests and fix session-0 assertion
Jul 15, 2026
fe42a77
ci(agent): run Windows agent tests without cgo
Jul 15, 2026
499864d
feat(watchdog): add verified Windows SCM recovery backend
Jul 15, 2026
d717d5d
ci(agent): scope Windows agent tests to green packages
Jul 15, 2026
de01ec4
fix(watchdog): journal and verify Windows recovery outcomes
Jul 15, 2026
4d4ae26
docs(agent): add Windows helper lifecycle rollout notes
Jul 15, 2026
5ce1851
fix(agent): remove data race between pre-auth close and peer credenti…
Jul 15, 2026
a1ed86a
docs(agent): point Windows test-debt references at #2523
Jul 15, 2026
dbc9ca6
docs(agent): point failed-kill respawn comment at #2530
Jul 15, 2026
6bcf403
fix(agent): spawn helpers on RD Session Host when Job Object assign i…
Jul 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 72 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,75 @@ jobs:
path: agent/coverage.out
retention-days: 7

test-agent-windows:
name: Test Agent (Windows)
runs-on: windows-latest
needs: [lint]
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: agent/go.sum

- name: Download Go dependencies
working-directory: agent
run: go mod download

# CGO_ENABLED=0 is required, not incidental. internal/remote/desktop is a
# non-cgo package on Windows: comVtblFn, dxgiCapturer and procDeleteObject
# are defined in files tagged `windows && !cgo`, while audio_windows.go,
# cursor_windows.go, gpu_convert_windows.go, monitor_windows.go and
# dxgi_dirty_rects_windows.go use them under a plain `windows` tag.
# windows-latest ships gcc, so CGO_ENABLED defaults to 1 there and the
# package fails to compile — and heartbeat/agentapp import it transitively,
# so they need cgo off too.
#
# This list is explicit rather than ./... because Windows CI has never run
# before this job, so //go:build windows tests across the repo have never
# executed anywhere and many are red. Verified on windows-latest and a
# Windows Server 2022 host, ALL pre-existing on main (none touched by the
# branch that added this job):
#
# internal/heartbeat - TestSendHelperTokenUpdateOnlyReachesAssistSessions,
# TestHandleHelperSessionAuthenticatedPushesOnlyToAssist,
# TestReconcileUserHelper_UnexpectedStatError_NoDownload,
# TestResolveRunAsSessionUserPrefersRunAsUserScope
# internal/agentapp - TestStaticUnitMatchesEmbedded
# internal/config - TestPersistedServerURLProviderFollowsPromotion
# also red under ./... : internal/backup{,/providers,/systemstate},
# peripheral, procoutput, remote/{desktop,filedrop,tools},
# updater
#
# Those packages are excluded so this gate reports on code that is actually
# green, rather than being red on inherited debt and therefore ignored.
# Fixing them and widening to ./... is tracked in #2523, which records the
# root cause of each failure.
#
# ./internal/eventlog IS listed and IS green: its tests previously ran on no
# platform at all (Linux skips them via build tag, and `go build` never
# compiles _test.go), which is the exact gap this job was added to close.
# When adding a package with //go:build windows tests, add it here.
- name: Run Windows Go tests
working-directory: agent
env:
CGO_ENABLED: "0"
run: go test ./internal/sessionbroker ./internal/eventlog ./internal/watchdog ./cmd/breeze-watchdog

# -race requires cgo, which remote/desktop cannot satisfy on Windows, so
# this covers only packages that do not import it (heartbeat and agentapp
# do, transitively). Same package set as the step above: sessionbroker is
# the concurrency-critical one — session-keyed helper admission, the
# lifecycle registry, and Job Object ownership all live there — so it is
# the one that most needs the detector on the OS it actually ships to.
- name: Run Windows race tests
working-directory: agent
run: go test -race ./internal/sessionbroker ./internal/eventlog ./internal/watchdog ./cmd/breeze-watchdog

# ─── Windows runtime smoke (#1000) ────────────────────────────────────
# CI cross-compiles the Windows agent (build-agent matrix) but never RAN
# it on Windows, so runtime-only Windows regressions shipped green. This
Expand Down Expand Up @@ -1319,7 +1388,7 @@ jobs:
ci-success:
name: CI Success
runs-on: ubuntu-latest
needs: [lint, typecheck, test-api, test-web, test-portal, test-office-addin-core, test-excel-addin, test-word-addin, test-powerpoint-addin, test-outlook-addin, security-audit, build-api, build-web, build-portal, build-agent, test-agent, windows-runtime-smoke, integration-test, rust-check, smoke-test]
needs: [lint, typecheck, test-api, test-web, test-portal, test-office-addin-core, test-excel-addin, test-word-addin, test-powerpoint-addin, test-outlook-addin, security-audit, build-api, build-web, build-portal, build-agent, test-agent, test-agent-windows, windows-runtime-smoke, integration-test, rust-check, smoke-test]
if: ${{ !cancelled() }}
steps:
- name: Check all jobs passed
Expand All @@ -1340,6 +1409,7 @@ jobs:
BUILD_PORTAL_RESULT: ${{ needs.build-portal.result }}
BUILD_AGENT_RESULT: ${{ needs.build-agent.result }}
TEST_AGENT_RESULT: ${{ needs.test-agent.result }}
TEST_AGENT_WINDOWS_RESULT: ${{ needs.test-agent-windows.result }}
WINDOWS_RUNTIME_SMOKE_RESULT: ${{ needs.windows-runtime-smoke.result }}
INTEGRATION_TEST_RESULT: ${{ needs.integration-test.result }}
RUST_CHECK_RESULT: ${{ needs.rust-check.result }}
Expand All @@ -1362,6 +1432,7 @@ jobs:
[[ "${BUILD_PORTAL_RESULT}" != "success" ]] || \
[[ "${BUILD_AGENT_RESULT}" != "success" ]] || \
[[ "${TEST_AGENT_RESULT}" != "success" ]] || \
[[ "${TEST_AGENT_WINDOWS_RESULT}" != "success" ]] || \
[[ "${WINDOWS_RUNTIME_SMOKE_RESULT}" != "success" ]] || \
[[ "${INTEGRATION_TEST_RESULT}" != "success" ]] || \
[[ "${RUST_CHECK_RESULT}" != "success" ]]; then
Expand Down
42 changes: 42 additions & 0 deletions agent/cmd/breeze-watchdog/failover_dispatch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,45 @@ func hasJournalEvent(entries []watchdog.JournalEntry, event string) bool {
}
return false
}

// An operator failover command carries its own intent. The watchdog must map
// the command type to that intent explicitly and never let the escalation
// ladder's attempt count decide: at attempt 2 the unhealthy ladder selects a
// forced restart (terminate the process, then start), which a "start_agent"
// command must never trigger. Ladder selection itself is proven against the
// controllers in internal/watchdog; these tests pin the command → intent map
// that feeds it.

func TestFailoverStartAgentUsesEnsureStartIntent(t *testing.T) {
intent, resetFirst, ok := failoverRecoveryIntent("start_agent")
if !ok {
t.Fatal("start_agent is not mapped to a recovery intent")
}
if intent != watchdog.RecoveryIntentEnsureStart {
t.Errorf("intent = %q, want %q", intent, watchdog.RecoveryIntentEnsureStart)
}
if resetFirst {
t.Error("start_agent reset the escalation window; only an operator restart may")
}
}

func TestFailoverRestartAgentUsesRestartIntent(t *testing.T) {
intent, resetFirst, ok := failoverRecoveryIntent("restart_agent")
if !ok {
t.Fatal("restart_agent is not mapped to a recovery intent")
}
if intent != watchdog.RecoveryIntentRestart {
t.Errorf("intent = %q, want %q", intent, watchdog.RecoveryIntentRestart)
}
if !resetFirst {
t.Error("restart_agent did not reset the escalation window before attempting")
}
}

func TestFailoverRecoveryIntentRejectsNonRecoveryCommands(t *testing.T) {
for _, cmdType := range []string{"collect_diagnostics", "update_agent", "", "start"} {
if intent, _, ok := failoverRecoveryIntent(cmdType); ok {
t.Errorf("%q mapped to recovery intent %q, want no mapping", cmdType, intent)
}
}
}
Loading
Loading