Phase 12.G+: Tentacle E2E hardening — 17→66 tests, 6 production-bug rounds, StubSquidServer infra
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.ymlworkflow — 66/66✅ onwindows-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
-
WindowsServiceHost.{Install,Start,Stop,Uninstall,Status}↔️ realsc.exe -
ServiceCommand.ExecuteAsyncuninstall+purge composition -
RegisterCommand.ExecuteAsync↔️ real HTTP REST + JSON config persistence -
LocalScriptService↔️ real Halibut RPC + real PowerShell/bash -
ServiceMessageParser.ParseOutputVariablesfrom real agent log capture -
WindowsTentacleUpgradeStrategy.BuildOuterWrapper↔️ real Task Scheduler -
WindowsTentacleUpgradeStrategy.UpgradeAsync↔️ full dispatch+observe+outcome mapping - Halibut
DataStreamfile 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 productionLocalScriptServicebehind 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.mdRule 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 |
|
| J.E.3+ | ~30-40 | 3-5 days |
|
| J.D.5 | ~10 | 1 day |
|
| K | ~40 | 2-3 days |
|
| L | ~28 | 1 day |
|
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.