feat(v0.6): route lease-freshness callsites through IsStale
InspectLease, CleanupStaleLease, runActiveLeaseCheck, and statusAt now use the PID-aware IsStale predicate. A dead local owner PID makes a lease stale immediately; SessionStatus / list / info reflect this with no display-code change. Corrects three cmd-package session-display test fixtures that built "active" leases with the local hostname but synthetic PIDs — now that freshness is PID-aware, an active session must be owned by a live process, so the fixtures use os.Getpid().
This commit is contained in:
@@ -2,6 +2,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -61,8 +62,12 @@ func TestInfoShowsActivePersistentSessionWithAttachHint(t *testing.T) {
|
||||
wsDir := makeInfoSessionWorkspace(t, root, "active-persist")
|
||||
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
// An active session is owned by a live process: lease freshness is
|
||||
// PID-aware (v0.6 Phase 3), so a local-hostname lease must point at a
|
||||
// live PID to read as active. Use the test process itself.
|
||||
livePID := os.Getpid()
|
||||
writeLeaseAtForCmdTest(t, wsDir, &session.Lease{
|
||||
PID: 12345,
|
||||
PID: livePID,
|
||||
Hostname: session.CurrentHostname(),
|
||||
Mode: "persistent",
|
||||
StartedAt: now,
|
||||
@@ -76,7 +81,7 @@ func TestInfoShowsActivePersistentSessionWithAttachHint(t *testing.T) {
|
||||
for _, want := range []string{
|
||||
"Session: active",
|
||||
"Mode: persistent",
|
||||
"Owner: pid 12345", // local host -> hostname omitted
|
||||
fmt.Sprintf("Owner: pid %d", livePID), // local host -> hostname omitted
|
||||
"Attach: ctask attach active-persist",
|
||||
} {
|
||||
if !strings.Contains(out, want) {
|
||||
@@ -91,8 +96,10 @@ func TestInfoShowsActiveDirectSessionWithoutAttachHint(t *testing.T) {
|
||||
wsDir := makeInfoSessionWorkspace(t, root, "active-direct")
|
||||
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
// Live PID required for an active local-hostname lease — see
|
||||
// TestInfoShowsActivePersistentSessionWithAttachHint.
|
||||
writeLeaseAtForCmdTest(t, wsDir, &session.Lease{
|
||||
PID: 8888,
|
||||
PID: os.Getpid(),
|
||||
Hostname: session.CurrentHostname(),
|
||||
Mode: "direct",
|
||||
StartedAt: now,
|
||||
|
||||
@@ -47,17 +47,25 @@ func TestListSessionColumnShowsModeAndStaleAndDash(t *testing.T) {
|
||||
staleWS := makeListSessionWorkspace(t, root, "general", "2026-05-12_stale-ws", "stale-ws", "active")
|
||||
makeListSessionWorkspace(t, root, "general", "2026-05-11_idle-ws", "idle-ws", "active")
|
||||
|
||||
// persist-ws and direct-ws must read as active sessions: a fresh
|
||||
// heartbeat alone is not enough now that lease freshness is PID-aware
|
||||
// (v0.6 Phase 3). Use the live test process PID so the local-hostname
|
||||
// lease passes the PID-liveness check, mirroring
|
||||
// session.TestInspectLeaseFreshLocal.
|
||||
livePID := os.Getpid()
|
||||
writeLeaseAtForCmdTest(t, persistWS, &session.Lease{
|
||||
PID: 1, Hostname: host, Mode: "persistent",
|
||||
PID: livePID, Hostname: host, Mode: "persistent",
|
||||
StartedAt: now, LastHeartbeatAt: now,
|
||||
})
|
||||
writeLeaseAtForCmdTest(t, directWS, &session.Lease{
|
||||
PID: 2, Hostname: host, Mode: "direct",
|
||||
PID: livePID, Hostname: host, Mode: "direct",
|
||||
StartedAt: now, LastHeartbeatAt: now,
|
||||
})
|
||||
// stale-ws is wall-clock stale (heartbeat 10m ago); its PID is
|
||||
// irrelevant — wall-clock staleness wins unconditionally.
|
||||
heartbeat := now.Add(-10 * time.Minute)
|
||||
writeLeaseAtForCmdTest(t, staleWS, &session.Lease{
|
||||
PID: 3, Hostname: host, Mode: "direct",
|
||||
PID: livePID, Hostname: host, Mode: "direct",
|
||||
StartedAt: heartbeat, LastHeartbeatAt: heartbeat,
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A fresh-by-wall-clock local lease whose owner PID is dead must be
|
||||
// classified LeaseStateStale by InspectLease, so the persistent-mode
|
||||
// dispatcher routes to adoption immediately rather than after the 60s
|
||||
// wall-clock wait. (dispatchPersistent itself lives in cmd/ and is
|
||||
// covered there; this test pins the InspectLease half of the contract.)
|
||||
func TestInspectLeaseDeadLocalPIDIsStale(t *testing.T) {
|
||||
withCheckProcess(t, func(int) ProcessState { return ProcessDead })
|
||||
ws := t.TempDir()
|
||||
now := time.Now().UTC()
|
||||
writeLeaseAt(t, ws, &Lease{
|
||||
SessionID: "test",
|
||||
PID: 4242,
|
||||
Hostname: currentHostname(),
|
||||
LastHeartbeatAt: now, // fresh by wall-clock
|
||||
StartedAt: now,
|
||||
})
|
||||
if got := InspectLease(ws); got != LeaseStateStale {
|
||||
t.Errorf("dead-PID local lease: InspectLease = %v, want LeaseStateStale", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The control case: a fresh local lease with a live PID stays FreshLocal,
|
||||
// so passive reattach (not adoption) is chosen.
|
||||
func TestInspectLeaseLiveLocalPIDIsFreshLocal(t *testing.T) {
|
||||
withCheckProcess(t, func(int) ProcessState { return ProcessAlive })
|
||||
ws := t.TempDir()
|
||||
now := time.Now().UTC()
|
||||
writeLeaseAt(t, ws, &Lease{
|
||||
SessionID: "test",
|
||||
PID: 4242,
|
||||
Hostname: currentHostname(),
|
||||
LastHeartbeatAt: now,
|
||||
StartedAt: now,
|
||||
})
|
||||
if got := InspectLease(ws); got != LeaseStateFreshLocal {
|
||||
t.Errorf("live-PID local lease: InspectLease = %v, want LeaseStateFreshLocal", got)
|
||||
}
|
||||
}
|
||||
@@ -189,7 +189,7 @@ func CleanupStaleLease(path string, staleAfter time.Duration) (*Lease, error) {
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
if IsFresh(&l, time.Now(), staleAfter) {
|
||||
if !IsStale(&l, time.Now(), staleAfter) {
|
||||
return nil, nil
|
||||
}
|
||||
if rmErr := os.Remove(path); rmErr != nil && !errors.Is(rmErr, os.ErrNotExist) {
|
||||
|
||||
@@ -56,7 +56,7 @@ func InspectLease(wsDir string) LeaseState {
|
||||
if l == nil {
|
||||
return LeaseStateNone
|
||||
}
|
||||
if !IsFresh(l, time.Now(), StaleLeaseAfter) {
|
||||
if IsStale(l, time.Now(), StaleLeaseAfter) {
|
||||
return LeaseStateStale
|
||||
}
|
||||
if l.Hostname != currentHostname() {
|
||||
|
||||
@@ -101,7 +101,7 @@ func runActiveLeaseCheck(opts PreflightOpts) (bool, bool, error) {
|
||||
existing, err := ReadLease(leasePath)
|
||||
switch {
|
||||
case err == nil && existing != nil:
|
||||
if IsFresh(existing, time.Now(), StaleLeaseAfter) {
|
||||
if !IsStale(existing, time.Now(), StaleLeaseAfter) {
|
||||
if opts.Force {
|
||||
return true, true, nil
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ func statusAt(wsDir string, now time.Time) Status {
|
||||
}
|
||||
|
||||
state := SessionStateStale
|
||||
if IsFresh(&l, now, StaleLeaseAfter) {
|
||||
if !IsStale(&l, now, StaleLeaseAfter) {
|
||||
state = SessionStateActive
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user