esengine--deepseek-reasonix
230 行
8.7 KiB
Go
230 行
8.7 KiB
Go
package agent
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"reasonix/internal/provider"
|
|
"reasonix/internal/store"
|
|
)
|
|
|
|
// forkRecoveryBranch builds a real conflict-recovery branch: a diverged disk
|
|
// parent plus a stale in-memory session, forked through SaveRecoveryBranch —
|
|
// the exact artifacts GC will meet in the field.
|
|
func forkRecoveryBranch(t *testing.T, dir, name string) (parentPath, branchPath string, branchMsgs []provider.Message) {
|
|
t.Helper()
|
|
parentPath = filepath.Join(dir, name+".jsonl")
|
|
disk := NewSession("sys")
|
|
disk.Add(provider.Message{Role: provider.RoleUser, Content: "first"})
|
|
disk.Add(provider.Message{Role: provider.RoleAssistant, Content: "one"})
|
|
disk.Add(provider.Message{Role: provider.RoleUser, Content: "disk " + name})
|
|
if err := disk.Save(parentPath); err != nil {
|
|
t.Fatalf("Save parent: %v", err)
|
|
}
|
|
stale := NewSession("sys")
|
|
stale.Add(provider.Message{Role: provider.RoleUser, Content: "first"})
|
|
stale.Add(provider.Message{Role: provider.RoleAssistant, Content: "one"})
|
|
stale.Add(provider.Message{Role: provider.RoleUser, Content: "local " + name})
|
|
info, err := stale.SaveRecoveryBranch(RecoveryBranchOptions{OriginalPath: parentPath})
|
|
if err != nil {
|
|
t.Fatalf("SaveRecoveryBranch: %v", err)
|
|
}
|
|
return parentPath, info.Path, stale.Snapshot()
|
|
}
|
|
|
|
// coverBranchInParent rewrites the parent so it contains the branch's content
|
|
// plus later turns — the "original session went on and kept everything the
|
|
// fork preserved" shape that makes the fork redundant.
|
|
func coverBranchInParent(t *testing.T, parentPath string, branchMsgs []provider.Message) {
|
|
t.Helper()
|
|
merged := NewSession("")
|
|
merged.Messages = append([]provider.Message(nil), branchMsgs...)
|
|
merged.Add(provider.Message{Role: provider.RoleAssistant, Content: "answered after recovery"})
|
|
if err := merged.Save(parentPath); err != nil {
|
|
t.Fatalf("Save covering parent: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestReclaimableRecoveryBranchesCollectsOnlyCoveredIdleForks(t *testing.T) {
|
|
dir := t.TempDir()
|
|
later := time.Now().Add(48 * time.Hour)
|
|
|
|
// Covered + idle + unleased: reclaimable.
|
|
_, covered, coveredMsgs := forkRecoveryBranch(t, dir, "covered")
|
|
coverBranchInParent(t, filepath.Join(dir, "covered.jsonl"), coveredMsgs)
|
|
|
|
// Diverged parent: the fork holds turns that exist nowhere else — kept.
|
|
forkRecoveryBranch(t, dir, "diverged")
|
|
|
|
// Continued on: one follow-up turn on the branch disqualifies it forever.
|
|
continuedParent, continuedBranch, continuedMsgs := forkRecoveryBranch(t, dir, "continued")
|
|
coverBranchInParent(t, continuedParent, continuedMsgs)
|
|
cont, err := LoadSession(continuedBranch)
|
|
if err != nil {
|
|
t.Fatalf("LoadSession continued branch: %v", err)
|
|
}
|
|
cont.Add(provider.Message{Role: provider.RoleAssistant, Content: "user kept chatting here"})
|
|
if err := cont.Save(continuedBranch); err != nil {
|
|
t.Fatalf("Save continued branch: %v", err)
|
|
}
|
|
|
|
got, err := ReclaimableRecoveryBranches(dir, later, RecoveryGCGracePeriod)
|
|
if err != nil {
|
|
t.Fatalf("ReclaimableRecoveryBranches: %v", err)
|
|
}
|
|
if len(got) != 1 || got[0] != covered {
|
|
t.Fatalf("reclaimable = %v, want only %q", got, covered)
|
|
}
|
|
}
|
|
|
|
func TestReclaimableRecoveryBranchesRespectsGraceLeaseAndMissingParent(t *testing.T) {
|
|
dir := t.TempDir()
|
|
later := time.Now().Add(48 * time.Hour)
|
|
|
|
parentPath, branchPath, branchMsgs := forkRecoveryBranch(t, dir, "guarded")
|
|
coverBranchInParent(t, parentPath, branchMsgs)
|
|
|
|
// Fresh fork inside the grace window: kept.
|
|
if got, err := ReclaimableRecoveryBranches(dir, time.Now(), RecoveryGCGracePeriod); err != nil || len(got) != 0 {
|
|
t.Fatalf("within grace = %v err=%v, want none", got, err)
|
|
}
|
|
|
|
// Lease held (by this very process): kept.
|
|
lease, err := TryAcquireSessionLease(branchPath)
|
|
if err != nil {
|
|
t.Fatalf("TryAcquireSessionLease: %v", err)
|
|
}
|
|
if !SessionLeaseHeld(branchPath) {
|
|
t.Fatal("SessionLeaseHeld = false while this process holds the lease")
|
|
}
|
|
if got, err := ReclaimableRecoveryBranches(dir, later, RecoveryGCGracePeriod); err != nil || len(got) != 0 {
|
|
t.Fatalf("with lease held = %v err=%v, want none", got, err)
|
|
}
|
|
lease.Release()
|
|
if SessionLeaseHeld(branchPath) {
|
|
t.Fatal("SessionLeaseHeld = true after release")
|
|
}
|
|
|
|
// Released + idle: reclaimable now.
|
|
if got, err := ReclaimableRecoveryBranches(dir, later, RecoveryGCGracePeriod); err != nil || len(got) != 1 || got[0] != branchPath {
|
|
t.Fatalf("after release = %v err=%v, want %q", got, err, branchPath)
|
|
}
|
|
|
|
// Parent gone: content is no longer covered anywhere — kept.
|
|
for _, suffix := range []string{"", ".events.jsonl", ".meta"} {
|
|
if err := os.Remove(parentPath + suffix); err != nil && !os.IsNotExist(err) {
|
|
t.Fatalf("remove parent artifact %s: %v", suffix, err)
|
|
}
|
|
}
|
|
if got, err := ReclaimableRecoveryBranches(dir, later, RecoveryGCGracePeriod); err != nil || len(got) != 0 {
|
|
t.Fatalf("parent missing = %v err=%v, want none", got, err)
|
|
}
|
|
}
|
|
|
|
func TestRecoveryBranchCoveredByParentReadsActualContent(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
_, divergedBranch, _ := forkRecoveryBranch(t, dir, "diverged-proof")
|
|
if RecoveryBranchCoveredByParent(divergedBranch, dir) {
|
|
t.Fatal("diverged parent reported as covering its recovery branch")
|
|
}
|
|
|
|
coveredParent, coveredBranch, coveredMsgs := forkRecoveryBranch(t, dir, "covered-proof")
|
|
coverBranchInParent(t, coveredParent, coveredMsgs)
|
|
if !RecoveryBranchCoveredByParent(coveredBranch, dir) {
|
|
t.Fatal("parent containing the recovery transcript did not cover the branch")
|
|
}
|
|
|
|
// Restore the fork metadata after continuing the transcript. This models a
|
|
// stale sidecar that still claims content_digest == recovery_digest even
|
|
// though the actual branch has changed.
|
|
staleMeta, ok, err := LoadBranchMeta(coveredBranch)
|
|
if err != nil || !ok {
|
|
t.Fatalf("LoadBranchMeta stale branch: ok=%v err=%v", ok, err)
|
|
}
|
|
continued, err := LoadSession(coveredBranch)
|
|
if err != nil {
|
|
t.Fatalf("LoadSession stale branch: %v", err)
|
|
}
|
|
continued.Add(provider.Message{Role: provider.RoleAssistant, Content: "continued after sidecar snapshot"})
|
|
if err := continued.Save(coveredBranch); err != nil {
|
|
t.Fatalf("Save continued branch: %v", err)
|
|
}
|
|
if err := SaveBranchMetaPreserveUpdated(coveredBranch, staleMeta); err != nil {
|
|
t.Fatalf("restore stale branch meta: %v", err)
|
|
}
|
|
if RecoveryBranchCoveredByParent(coveredBranch, dir) {
|
|
t.Fatal("stale sidecar authorized cleanup of changed branch content")
|
|
}
|
|
|
|
missingParent, missingBranch, missingMsgs := forkRecoveryBranch(t, dir, "missing-proof")
|
|
coverBranchInParent(t, missingParent, missingMsgs)
|
|
if !RecoveryBranchCoveredByParent(missingBranch, dir) {
|
|
t.Fatal("missing-parent fixture was not covered before parent removal")
|
|
}
|
|
for _, suffix := range []string{"", ".events.jsonl", ".meta"} {
|
|
if err := os.Remove(missingParent + suffix); err != nil && !os.IsNotExist(err) {
|
|
t.Fatalf("remove parent artifact %s: %v", suffix, err)
|
|
}
|
|
}
|
|
if RecoveryBranchCoveredByParent(missingBranch, dir) {
|
|
t.Fatal("missing parent reported as covering its recovery branch")
|
|
}
|
|
}
|
|
|
|
func TestRecoveryParentGuardBlocksRewindAfterValidation(t *testing.T) {
|
|
dir := t.TempDir()
|
|
parentPath, branchPath, branchMsgs := forkRecoveryBranch(t, dir, "rewind-race")
|
|
coverBranchInParent(t, parentPath, branchMsgs)
|
|
|
|
guard, err := TryAcquireRecoveryParentGuard(branchPath, dir)
|
|
if err != nil {
|
|
t.Fatalf("TryAcquireRecoveryParentGuard: %v", err)
|
|
}
|
|
// Force the dangerous ordering: coverage validation has completed, then a
|
|
// concurrent rewind tries to take the parent's save lock before purge. The
|
|
// guard must keep that lock unavailable until the caller finishes deleting
|
|
// the redundant branch.
|
|
if lock, err := tryTakeSessionLockFile(store.SessionLockFile(parentPath)); !errors.Is(err, errSessionFileLockHeld) {
|
|
if lock != nil {
|
|
lock.Unlock()
|
|
}
|
|
guard.Release()
|
|
t.Fatalf("parent save lock after coverage validation = %v, want errSessionFileLockHeld", err)
|
|
}
|
|
guard.Release()
|
|
|
|
parent, err := LoadSession(parentPath)
|
|
if err != nil {
|
|
t.Fatalf("LoadSession parent: %v", err)
|
|
}
|
|
parent.Messages = append([]provider.Message(nil), parent.Messages[:1]...)
|
|
if err := parent.SaveRewrite(parentPath); err != nil {
|
|
t.Fatalf("SaveRewrite after guard release: %v", err)
|
|
}
|
|
if RecoveryBranchCoveredByParent(branchPath, dir) {
|
|
t.Fatal("rewound parent still reported as covering the recovery branch")
|
|
}
|
|
}
|
|
|
|
func TestRecoveryParentGuardRefusesInFlightParentRewrite(t *testing.T) {
|
|
dir := t.TempDir()
|
|
parentPath, branchPath, branchMsgs := forkRecoveryBranch(t, dir, "rewrite-busy")
|
|
coverBranchInParent(t, parentPath, branchMsgs)
|
|
|
|
unlock, err := lockSessionFile(parentPath)
|
|
if err != nil {
|
|
t.Fatalf("lockSessionFile: %v", err)
|
|
}
|
|
defer unlock()
|
|
if guard, err := TryAcquireRecoveryParentGuard(branchPath, dir); !errors.Is(err, ErrSessionLeaseHeld) {
|
|
if guard != nil {
|
|
guard.Release()
|
|
}
|
|
t.Fatalf("guard during parent rewrite err = %v, want ErrSessionLeaseHeld", err)
|
|
}
|
|
}
|