Skip to content

Phase 12.G+: Tentacle E2E hardening — 17→66 tests, 6 production-bug rounds, StubSquidServer infra

Placeholder ppxd requested to merge phase12.G-real-e2e-hardening into main

Summary

Comprehensive Tentacle E2E coverage refresh: started at 17 tests all failing on the Windows runner, ending at 66 tests all green on Windows + macOS.

Six rounds of round-trip fixes caught real Windows-specific production behaviour that bash on macOS/Linux didn't expose. One genuine production bug found and fixed in WindowsServiceHost.BuildScCreateArgs (sc.exe argv token splitting).

Coverage delta:

  • +49 tests across 7 production code paths
  • 31 tests genuinely cross-OS (run on macOS dev boxes, not skip-guarded)
  • 6 round-trip stability fixes baked into the test design

Test plan

  • dotnet test tests/Squid.WindowsUpgradeE2ETests — 66/66 ✅ on macOS
  • dotnet test tests/Squid.UnitTests — 5003/5003 ✅ on macOS
  • dotnet test tests/Squid.Tentacle.Tests (Windows category) — 5/5 ✅ on Windows runner
  • tentacle-windows-e2e.yml workflow — 66/66 ✅ on windows-latest (run 25421624407)

Phases shipped

Phase Tests What it covers
G rounds 1-6 17 fixed + 0 new All 14 prior failures → green; sc.exe argv prod bug; Task Scheduler XML; UTF-8 BOM; SCM async state; concurrent race; PS multi-emit
G.2 +12 WindowsServiceHost SCM lifecycle (Install/Start/Stop/Uninstall/Status + restart policy)
G.5 +3 ServiceCommand uninstall --purge composition (SCM + filesystem + InstanceRegistry)
H +8 StubSquidServer foundation (Halibut polling + REST register endpoint + cert)
I +10 RegisterCommand handshake (Listening + Polling + 401 + unreachable + named instance + multi-role + bearer token)
J.D.1 +5 Server→agent→shell happy paths (echo, exit code, polling round-trip, multi-line, stderr)
J.D.2 +3 Long-running (3s sleep), concurrent dispatches, unicode (CJK + em-dash + emoji)
J.D.3 +3 ##squid[setVariable] round-trip via production ServiceMessageParser (plain, sensitive, base64)
J.D.4 +2 File transfer via ScriptFile[] + DataStream (single + multi-file)
J.E.1 +3 WindowsTentacleUpgradeStrategy.UpgradeAsync end-to-end (Windows-only — uses real Task Scheduler)
Total 66 ✅

Production code paths now exercised end-to-end

🟢 High-fidelity (real prod class + real OS resource):

  • WindowsServiceHost.{Install,Start,Stop,Uninstall,Status} ↔️ real sc.exe
  • ServiceCommand.ExecuteAsync uninstall+purge composition
  • RegisterCommand.ExecuteAsync ↔️ real HTTP REST + JSON config persistence
  • LocalScriptService ↔️ real Halibut RPC + real PowerShell/bash
  • ServiceMessageParser.ParseOutputVariables from real agent log capture
  • WindowsTentacleUpgradeStrategy.BuildOuterWrapper ↔️ real Task Scheduler
  • WindowsTentacleUpgradeStrategy.UpgradeAsync ↔️ full dispatch+observe+outcome mapping
  • Halibut DataStream file transfer to agent workDir
  • Per-script-ticket isolation under concurrent dispatch
  • UTF-8 unicode round-trip through Halibut + shell
  • 6 rounds of Windows-runner-discovered timing/encoding/race fixes

Production bug fixed

WindowsServiceHost.BuildScCreateArgs — sc.exe requires key= and value as separate argv tokens (not packed as a single \"key= value\" string). Pre-fix, every Windows operator running squid-tentacle service install hit [SC] CreateService FAILED 1639: invalid argument. Caught by G.2 E2E + verified by test-pinning the new argv shape.

New shared infrastructure

  • tests/Squid.WindowsUpgradeE2ETests/Infrastructure/StubSquidServer.cs — in-process Halibut polling listener + HTTP REST endpoint for register handshake. Public surface: StartAsync(), ServerThumbprint, PollingUri, ServerUri, TrustAgent(), DispatchAndObserveListeningAsync(), DispatchAndObservePollingAsync(), ConfigureRegisterStatusCode(), ReceivedRegistrations.
  • tests/Squid.WindowsUpgradeE2ETests/Infrastructure/StubAgent.cs — wraps production LocalScriptService behind a Halibut runtime. Public surface: StartListeningAsync(), StartPollingAsync(), Thumbprint, ListeningUri, SubscriptionId.

Both fixtures are cross-platform (Halibut + LocalScriptService run on Windows / Linux / macOS). Tests using them run identically on every dev OS — no skip-guards needed for the cross-OS scenarios.

Discipline locked in

  • ~/.claude/CLAUDE.md Rule 12 — E2E fidelity tiers (🟢 high / 🟡 medium-mirror / 🟡 medium-mock / 🔵 fixture-only) + canonical test pattern + 10 hard rules
  • Project CLAUDE.md — Tentacle E2E section with phase plan, naming conventions, drift-detector pattern
  • docs/e2e-scenario-matrix.md — 141-scenario / ≈201-test ledger; 66 marked Covered with test-name references; remaining scenarios mapped to future phases (J.E.2, K, L)

Discovered docs/impl divergence (flagged for separate task)

install-tentacle.ps1 doc-string says --role can be passed multiple times; actual Microsoft.Extensions.Configuration.CommandLine keeps only the last value. Working UX is comma-separated. Pinned current behaviour as RepeatedRoleFlags_OnlyLastValueWins_KnownBug regression-pin so a future fix flips the test red and forces the docs to update in lockstep.

What's left for future sessions

Phase Tests Effort Priority
J.E.2 ~12 1 day 🔴 High — capabilities probe + last-upgrade.json + release mirror
J.E.3+ ~30-40 3-5 days 🟡 Medium — upgrade methods (zip/apt/dnf/tarball) + rollback + lock
J.D.5 ~10 1 day 🟡 Medium — Calamari + variable substitution + cancellation
K ~40 2-3 days 🟡 Medium — install scripts + boundary cases
L ~28 1 day 🟢 Low — lifecycle remainder + health + multi-instance

These phases land in their own branches off post-merge main. The infrastructure is in place; subsequent phases are "filling in" tests against established fixtures, not building new plumbing.

🤖 Generated with Claude Code

Merge request reports

Loading