feat(v0.5.3): InspectLease four-state classifier
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LeaseState classifies the lease found at a workspace's session.json. It is
|
||||
// the input to the persistent-mode dispatcher: missing/stale/remote -> adopt;
|
||||
// fresh local -> passive reattach.
|
||||
type LeaseState int
|
||||
|
||||
const (
|
||||
// LeaseStateNone: lease file missing or unparseable.
|
||||
LeaseStateNone LeaseState = iota
|
||||
// LeaseStateFreshLocal: lease parses, last_heartbeat_at < StaleLeaseAfter,
|
||||
// hostname matches the current host.
|
||||
LeaseStateFreshLocal
|
||||
// LeaseStateStale: lease parses, last_heartbeat_at >= StaleLeaseAfter.
|
||||
LeaseStateStale
|
||||
// LeaseStateFreshRemote: lease parses, last_heartbeat_at < StaleLeaseAfter,
|
||||
// hostname differs from the current host.
|
||||
LeaseStateFreshRemote
|
||||
)
|
||||
|
||||
func (s LeaseState) String() string {
|
||||
switch s {
|
||||
case LeaseStateNone:
|
||||
return "none"
|
||||
case LeaseStateFreshLocal:
|
||||
return "fresh_local"
|
||||
case LeaseStateStale:
|
||||
return "stale"
|
||||
case LeaseStateFreshRemote:
|
||||
return "fresh_remote"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// InspectLease reads <wsDir>/.ctask/session.json and classifies the result.
|
||||
// Reuses the existing 60-second freshness threshold (StaleLeaseAfter) — the
|
||||
// persistent-mode dispatcher must agree with v0.4 lease semantics.
|
||||
func InspectLease(wsDir string) LeaseState {
|
||||
l, err := ReadLease(LeasePath(wsDir))
|
||||
if err != nil {
|
||||
// Missing or corrupt -> none. (CleanupStaleLease handles removal of
|
||||
// corrupt files when sessions actually start; for a read-only
|
||||
// inspection we just classify and return.)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return LeaseStateNone
|
||||
}
|
||||
return LeaseStateNone
|
||||
}
|
||||
if l == nil {
|
||||
return LeaseStateNone
|
||||
}
|
||||
if !IsFresh(l, time.Now(), StaleLeaseAfter) {
|
||||
return LeaseStateStale
|
||||
}
|
||||
if l.Hostname != currentHostname() {
|
||||
return LeaseStateFreshRemote
|
||||
}
|
||||
return LeaseStateFreshLocal
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func writeLeaseAt(t *testing.T, wsDir string, l *Lease) {
|
||||
t.Helper()
|
||||
if err := WriteLease(LeasePath(wsDir), l); err != nil {
|
||||
t.Fatalf("WriteLease: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectLeaseNoneWhenMissing(t *testing.T) {
|
||||
ws := t.TempDir()
|
||||
if got := InspectLease(ws); got != LeaseStateNone {
|
||||
t.Errorf("missing: got %v, want %v", got, LeaseStateNone)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectLeaseNoneWhenCorrupt(t *testing.T) {
|
||||
ws := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(ws, ".ctask"), 0755); err != nil {
|
||||
t.Fatalf("mkdir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(LeasePath(ws), []byte("not json"), 0644); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
if got := InspectLease(ws); got != LeaseStateNone {
|
||||
t.Errorf("corrupt: got %v, want %v", got, LeaseStateNone)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectLeaseFreshLocal(t *testing.T) {
|
||||
ws := t.TempDir()
|
||||
host := currentHostname()
|
||||
l := &Lease{
|
||||
SessionID: "test",
|
||||
PID: os.Getpid(),
|
||||
Hostname: host,
|
||||
Username: "u",
|
||||
Agent: "claude",
|
||||
Mode: "local",
|
||||
StartedAt: time.Now().UTC(),
|
||||
LastHeartbeatAt: time.Now().UTC(),
|
||||
Terminal: "test",
|
||||
}
|
||||
writeLeaseAt(t, ws, l)
|
||||
if got := InspectLease(ws); got != LeaseStateFreshLocal {
|
||||
t.Errorf("fresh local: got %v, want %v", got, LeaseStateFreshLocal)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectLeaseStale(t *testing.T) {
|
||||
ws := t.TempDir()
|
||||
host := currentHostname()
|
||||
l := &Lease{
|
||||
SessionID: "test",
|
||||
PID: os.Getpid(),
|
||||
Hostname: host,
|
||||
Username: "u",
|
||||
Agent: "claude",
|
||||
Mode: "local",
|
||||
StartedAt: time.Now().UTC().Add(-10 * time.Minute),
|
||||
LastHeartbeatAt: time.Now().UTC().Add(-10 * time.Minute),
|
||||
Terminal: "test",
|
||||
}
|
||||
writeLeaseAt(t, ws, l)
|
||||
if got := InspectLease(ws); got != LeaseStateStale {
|
||||
t.Errorf("stale: got %v, want %v", got, LeaseStateStale)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectLeaseFreshRemote(t *testing.T) {
|
||||
ws := t.TempDir()
|
||||
other := "some-other-host-that-is-not-this-one"
|
||||
if other == currentHostname() {
|
||||
other = "different-" + other
|
||||
}
|
||||
l := &Lease{
|
||||
SessionID: "test",
|
||||
PID: 1,
|
||||
Hostname: other,
|
||||
Username: "u",
|
||||
Agent: "claude",
|
||||
Mode: "local",
|
||||
StartedAt: time.Now().UTC(),
|
||||
LastHeartbeatAt: time.Now().UTC(),
|
||||
Terminal: "test",
|
||||
}
|
||||
writeLeaseAt(t, ws, l)
|
||||
if got := InspectLease(ws); got != LeaseStateFreshRemote {
|
||||
t.Errorf("fresh remote: got %v, want %v", got, LeaseStateFreshRemote)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectLeaseStringerCoverage(t *testing.T) {
|
||||
for _, s := range []LeaseState{LeaseStateNone, LeaseStateFreshLocal, LeaseStateStale, LeaseStateFreshRemote} {
|
||||
if s.String() == "" {
|
||||
t.Errorf("LeaseState %d has empty String()", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user