feat(switcher): ensure shared symlinks on target home after flip (A3)
Wire symlinks.EnsureForAccount into executeSwitch, called immediately after the ~/.claude flip. Guarantees the three shared-state links (session-env, file-history, projects) exist on the target account home even for freshly-provisioned accounts, preventing silent transcript duplication and undo-history divergence on first resume. Best-effort: errors are logged as WARN but never abort the swap. If we returned here the daemon would be left inconsistent (symlink flipped, SetActiveAccount never called). Operator sees the warning in logs and resolves divergent links manually. Tests: - TestFlipReconcilesSharedSymlinksOnTargetHome: empty target home gets all three links pointing at canonical targets after the flip. - TestFlipEnsureSymlinksFailureDoesNotAbortSwap: a planted divergent link triggers the symlinks-package error; the swap completes anyway and the active account is updated. Hermetic: added AccountSwitcher.sharedSymlinks override so tests scope the reconcile inside t.TempDir() and never touch /home/ubuntu/.claude-*-shared. Existing tests migrated to this pattern and hardcoded /tmp/claude-*-xxxx paths replaced with tmpdirs. Phase 1 / Chantier A — task A3.
This commit is contained in:
parent
e16e3526a0
commit
8eaf0bbd35
3 changed files with 204 additions and 4 deletions
|
|
@ -17,6 +17,7 @@ import (
|
|||
"forge.secuaas.ovh/olivier/claude-failover/internal/notify"
|
||||
"forge.secuaas.ovh/olivier/claude-failover/internal/quota"
|
||||
"forge.secuaas.ovh/olivier/claude-failover/internal/state"
|
||||
"forge.secuaas.ovh/olivier/claude-failover/internal/symlinks"
|
||||
"forge.secuaas.ovh/olivier/claude-failover/internal/tmux"
|
||||
)
|
||||
|
||||
|
|
@ -52,6 +53,11 @@ type AccountSwitcher struct {
|
|||
// homeDir is the directory containing the .claude symlink. Overridable for tests.
|
||||
// When empty, os.UserHomeDir() is used.
|
||||
homeDir string
|
||||
// sharedSymlinks is the list of shared-state links reconciled on the
|
||||
// target account home after every flip. Overridable for tests so the
|
||||
// suite never touches the operator's real /home/ubuntu/.claude-*
|
||||
// shared directories. When nil, symlinks.RequiredShared is used.
|
||||
sharedSymlinks []symlinks.SharedSymlink
|
||||
}
|
||||
|
||||
// New creates an AccountSwitcher.
|
||||
|
|
@ -110,6 +116,16 @@ func (a *AccountSwitcher) executeSwitch(req quota.SwitchRequest) {
|
|||
if err := a.flipSymlink(target.Home); err != nil {
|
||||
a.logger.Printf("[switcher] flipSymlink error: %v", err)
|
||||
}
|
||||
// Best-effort: make sure the target account home exposes the three
|
||||
// shared-state symlinks (session-env, file-history, projects). The main
|
||||
// ~/.claude flip is already done, so an error here must NOT abort the
|
||||
// swap — we just log it so the operator can investigate. Without this
|
||||
// call, a fresh target account with no shared links would silently
|
||||
// start writing into private /projects/session-env/file-history dirs
|
||||
// and diverge from the primary account's transcripts.
|
||||
if err := symlinks.EnsureForAccount(target.Home, a.requiredShared()); err != nil {
|
||||
a.logger.Printf("[switcher] WARN ensure shared symlinks for %q: %v", target.Home, err)
|
||||
}
|
||||
a.killAllPoolSessions()
|
||||
a.recreatePoolSessions()
|
||||
a.relaunchDedicatedSessions(target.Home)
|
||||
|
|
@ -225,6 +241,16 @@ func (a *AccountSwitcher) saveAllSessions() {
|
|||
})
|
||||
}
|
||||
|
||||
// requiredShared returns the shared-symlink list used when reconciling the
|
||||
// target account home after a flip. Tests may set a.sharedSymlinks to a
|
||||
// tmpdir-scoped list so they never touch /home/ubuntu/.claude-*-shared.
|
||||
func (a *AccountSwitcher) requiredShared() []symlinks.SharedSymlink {
|
||||
if a.sharedSymlinks != nil {
|
||||
return a.sharedSymlinks
|
||||
}
|
||||
return symlinks.RequiredShared
|
||||
}
|
||||
|
||||
// resolveHomeDir returns the configured homeDir (test override) or the real
|
||||
// user home. Tests MUST set a.homeDir to a tmpdir to avoid clobbering the
|
||||
// production ~/.claude symlink.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue