feat(LinuxTentacleE2E): Phase 12.L.E.8 — E15.h-Linux config preservation across upgrade
Summary
Linux mirror of the Windows E15.h ship-blocker (PR #198). Pins the contract that upgrade-linux-tentacle.sh's Phase B mv swap operates on INSTALL_DIR ONLY — sibling /etc/squid-tentacle/{instances.json, instances/<name>/...} MUST survive byte-for-byte.
Why ship-blocking
Without this test, a future polish that "tidies up" the upgrade flow could shred operator identity material:
| File path (Linux production) | Holds | Lost-without-pin failure mode |
|---|---|---|
/etc/squid-tentacle/instances.json |
Registry: name → ConfigPath map for every instance | Server sees every Tentacle "disappear"; operators re-register all |
/etc/squid-tentacle/instances/<name>.config.json |
Server URL + thumbprint + subscription ID | Agent can't dial in post-restart; service marks itself idle |
/etc/squid-tentacle/instances/<name>/certs/<...> |
Per-instance mTLS material | Polling subscription rejected at TLS handshake; permanent disconnection |
Test approach
Same pattern as Windows E15.h:
- Stage v1 service with healthz responder enabled (Phase B needs
200/OK). - Pre-stage instance state in a test-private config dir (sibling to INSTALL_DIR, not under host
/etc/). - SHA256 each file (pre-upgrade hashes).
- Run a normal v1→v2 upgrade (same shape as E1.h-Linux).
- Assert
exitCode == 0+ marker swapped to v2 — proves Phase B actually ran (without this, preservation assertions would pass vacuously). - Assert all three files still exist + SHA256 unchanged.
Why test-private config dir, not real /etc/squid-tentacle?
The .sh has no config-dir env override — it genuinely never reads or writes outside INSTALL_DIR + STATE_DIR. So any sibling tree is provably untouched simply by NOT being INSTALL_DIR. A test-private dir under Path.GetTempPath() pins the same structural contract without polluting the host's real /etc/squid-tentacle.
Infrastructure additions
LinuxLifecycleContext:
-
ConfigDirOverrideproperty (lazy-created on firstStageInstanceState) -
StageInstanceState(instanceName)→ pre-stages the three files, returnsInstanceConfigPathsrecord -
HashFile(path)static helper (SHA256 lower-hex) -
Disposenow best-effort cleans upConfigDirOverrideif present
Fidelity tier
.sh + real systemd-run --scope + real sudo + real bash + real LocalReleaseMirror + real OS filesystem hashing. No mocks at any layer.
Test plan
-
Linux E2E workflow runs Squid.LinuxTentacleE2ETeststo completion (manualworkflow_dispatchafter merge — workflow doesn't auto-trigger on PRs) -
E15h_UpgradePreservesInstanceConfigAndCertFiles_Linuxpasses -
No regression on E1.h-Linux / E1.u1-Linux / E12.u1-Linux (same fixture; preserves J.L.E.7 stability)