mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 02:36:01 -10:00
268 lines
9.3 KiB
Go
268 lines
9.3 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"reasonix/internal/state/sessionstore"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"reasonix/internal/base/testenv"
|
|
"reasonix/internal/contract/event"
|
|
"reasonix/internal/contract/provider"
|
|
)
|
|
|
|
func TestCompactionStateAtomicSaveLoad(t *testing.T) {
|
|
dir := testenv.TempDir(t)
|
|
path := filepath.Join(dir, "sess.jsonl")
|
|
st := sessionstore.CompactionState{
|
|
SchemaVersion: sessionstore.CompactionStateSchemaCurrent,
|
|
TranscriptVersion: 3,
|
|
Projection: sessionstore.ContextProjection{
|
|
Messages: []provider.Message{
|
|
{Role: provider.RoleSystem, Content: "sys"},
|
|
{Role: provider.RoleUser, Content: "summary-body"},
|
|
},
|
|
TranscriptVersion: 3,
|
|
ProjectionVersion: 1,
|
|
CoveredCount: 10,
|
|
SummaryHash: summaryContentHash("summary-body"),
|
|
SourceTokens: 1000,
|
|
ProjectionTokens: 200,
|
|
},
|
|
PromptCacheKey: "ws|sess|model",
|
|
LastCacheState: CacheStateCold,
|
|
Generation: 7,
|
|
LastReceipt: &sessionstore.ContextMaintenanceReceipt{
|
|
Status: "applied", Action: "summary", ProjectionVersion: 1,
|
|
InputHash: "in", OutputHash: "out", SavedTokens: 800,
|
|
},
|
|
}
|
|
if err := sessionstore.SaveCompactionState(path, st); err != nil {
|
|
t.Fatalf("save: %v", err)
|
|
}
|
|
got, ok, err := sessionstore.LoadCompactionState(path)
|
|
if err != nil || !ok {
|
|
t.Fatalf("load: ok=%v err=%v", ok, err)
|
|
}
|
|
if got.SchemaVersion != sessionstore.CompactionStateSchemaCurrent || got.TranscriptVersion != 3 {
|
|
t.Fatalf("loaded state = %+v", got)
|
|
}
|
|
if len(got.Projection.Messages) != 2 || got.Projection.CoveredCount != 10 {
|
|
t.Fatalf("projection = %+v", got.Projection)
|
|
}
|
|
if got.Generation != 7 || got.LastReceipt == nil || got.LastReceipt.OutputHash != "out" || got.LastReceipt.ProjectionVersion != 1 {
|
|
t.Fatalf("v3 maintenance receipt not round-tripped: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestLoadCompactionStateAcceptsLegacyV1(t *testing.T) {
|
|
path := filepath.Join(testenv.TempDir(t), "legacy.jsonl")
|
|
legacy := sessionstore.CompactionState{
|
|
SchemaVersion: sessionstore.CompactionStateSchemaV1,
|
|
TranscriptVersion: 2,
|
|
Projection: sessionstore.ContextProjection{
|
|
Messages: []provider.Message{{Role: provider.RoleUser, Content: "legacy summary"}},
|
|
TranscriptVersion: 2,
|
|
ProjectionVersion: 1,
|
|
CoveredCount: 3,
|
|
},
|
|
LastTrigger: CompactionTriggerManual,
|
|
}
|
|
raw, err := json.Marshal(legacy)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(sessionstore.ContextStatePath(path), raw, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got, ok, err := sessionstore.LoadCompactionState(path)
|
|
if err != nil || !ok {
|
|
t.Fatalf("load legacy V1: ok=%v err=%v", ok, err)
|
|
}
|
|
if got.SchemaVersion != sessionstore.CompactionStateSchemaV1 || got.Projection.Messages[0].Content != "legacy summary" {
|
|
t.Fatalf("legacy state changed: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestSaveCompactionStateCreatesPreviousReaderBoundary(t *testing.T) {
|
|
path := filepath.Join(testenv.TempDir(t), "current.jsonl")
|
|
if err := sessionstore.SaveCompactionState(path, sessionstore.CompactionState{
|
|
Projection: sessionstore.ContextProjection{
|
|
Messages: []provider.Message{{Role: provider.RoleUser, Content: "logical summary"}, {Role: provider.RoleUser, Content: "retained anchor"}},
|
|
CoveredCount: 2,
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, err := os.ReadFile(sessionstore.ContextStatePath(path))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var header struct {
|
|
SchemaVersion int `json:"schema_version"`
|
|
}
|
|
if err := json.Unmarshal(raw, &header); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if header.SchemaVersion != sessionstore.CompactionStateSchemaCurrent {
|
|
t.Fatalf("written schema = %d, want %d", header.SchemaVersion, sessionstore.CompactionStateSchemaCurrent)
|
|
}
|
|
if previousCompactionReaderAccepts(raw) {
|
|
t.Fatal("V1-only reader would accept a sidecar with V2 logical message invariants")
|
|
}
|
|
if _, ok, err := sessionstore.LoadCompactionState(path); err != nil || !ok {
|
|
t.Fatalf("current reader rejected V2 sidecar: ok=%v err=%v", ok, err)
|
|
}
|
|
}
|
|
|
|
func previousCompactionReaderAccepts(raw []byte) bool {
|
|
var header struct {
|
|
SchemaVersion int `json:"schema_version"`
|
|
}
|
|
if json.Unmarshal(raw, &header) != nil {
|
|
return false
|
|
}
|
|
return header.SchemaVersion == 0 || header.SchemaVersion == sessionstore.CompactionStateSchemaV1
|
|
}
|
|
|
|
func TestCompactToProjectionLeavesCanonicalIntact(t *testing.T) {
|
|
fp := &fakeProvider{reply: "GOAL: ship projection\nFACTS: keep path /tmp/x"}
|
|
sess := sessionstore.NewSession("sys")
|
|
// Build enough history that a fold is economical.
|
|
for i := range 12 {
|
|
sess.Add(provider.Message{Role: provider.RoleUser, Content: "user turn " + strings.Repeat("x", 80) + " " + string(rune('A'+i%26))})
|
|
sess.Add(provider.Message{Role: provider.RoleAssistant, Content: "assistant work " + strings.Repeat("y", 200)})
|
|
sess.Add(provider.Message{Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{ID: "c" + string(rune('0'+i%10)), Name: "read", Arguments: `{"path":"f"}`}}})
|
|
sess.Add(provider.Message{Role: provider.RoleTool, ToolCallID: "c" + string(rune('0'+i%10)), Name: "read", Content: strings.Repeat("tool-out-", 40)})
|
|
}
|
|
before := append([]provider.Message(nil), sess.Messages...)
|
|
dir := testenv.TempDir(t)
|
|
sessionPath := filepath.Join(dir, "s.jsonl")
|
|
a := New(fp, nil, sess, Options{
|
|
ContextWindow: 50_000,
|
|
CompactRatio: 0.85,
|
|
SessionPath: sessionPath,
|
|
ModelRef: "test/model",
|
|
}, event.Discard)
|
|
|
|
if _, err := a.CompactNow(context.Background(), CompactRequest{}); err != nil {
|
|
t.Fatalf("CompactNow: %v", err)
|
|
}
|
|
after := sess.Snapshot()
|
|
if len(after) != len(before) {
|
|
t.Fatalf("canonical length changed: before=%d after=%d", len(before), len(after))
|
|
}
|
|
for i := range before {
|
|
if before[i].Content != after[i].Content || before[i].Role != after[i].Role {
|
|
t.Fatalf("canonical message %d changed", i)
|
|
}
|
|
}
|
|
if len(a.sess.win.compactionState.Projection.Messages) == 0 {
|
|
t.Fatal("expected projection messages")
|
|
}
|
|
// Projection must be shorter than canonical.
|
|
if a.window().estimatedPromptTokens(a.sess.win.compactionState.Projection.Messages) >= a.window().estimatedPromptTokens(before) {
|
|
t.Fatalf("projection did not shrink: proj=%d src=%d",
|
|
a.window().estimatedPromptTokens(a.sess.win.compactionState.Projection.Messages),
|
|
a.window().estimatedPromptTokens(before))
|
|
}
|
|
// Sidecar must exist and reload with an applied summary receipt (v3 does not
|
|
// persist the legacy last_mode field).
|
|
st, ok, err := sessionstore.LoadCompactionState(sessionPath)
|
|
if err != nil || !ok {
|
|
t.Fatalf("reload sidecar: ok=%v err=%v", ok, err)
|
|
}
|
|
if st.LastReceipt == nil || st.LastReceipt.Status != "applied" || st.LastReceipt.Action != "summary" {
|
|
t.Fatalf("last receipt = %+v, want applied summary", st.LastReceipt)
|
|
}
|
|
if st.Projection.ProjectionVersion == 0 {
|
|
t.Fatal("reloaded projection version is zero")
|
|
}
|
|
// Model-visible must use projection.
|
|
visible := a.window().modelVisibleMessages()
|
|
if len(visible) == len(before) {
|
|
t.Fatal("model-visible still full canonical")
|
|
}
|
|
if len(fp.got) != 0 {
|
|
t.Fatalf("the fold made %d provider request(s); it archives instead", len(fp.got))
|
|
}
|
|
}
|
|
|
|
func earlyUserPrefix(msgs []provider.Message) string {
|
|
var b strings.Builder
|
|
for _, m := range msgs {
|
|
if m.Role == provider.RoleSystem {
|
|
continue
|
|
}
|
|
if isCompactionSummary(m) {
|
|
break
|
|
}
|
|
if m.Role == provider.RoleUser {
|
|
b.WriteString(m.Content)
|
|
b.WriteByte('\n')
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// Checkpoint installation no longer writes archive copies — the canonical
|
|
// transcript is the lossless store. ArchiveDir misconfiguration must not block
|
|
// a successful summary install.
|
|
func TestArchiveDirIgnoredOnCheckpointInstall(t *testing.T) {
|
|
fp := &fakeProvider{reply: "digest"}
|
|
sess := sessionstore.NewSession("sys")
|
|
big := strings.Repeat("assistant work detail ", 200)
|
|
for range 6 {
|
|
sess.Add(provider.Message{Role: provider.RoleUser, Content: "turn"})
|
|
sess.Add(provider.Message{Role: provider.RoleAssistant, Content: big})
|
|
}
|
|
sess.Add(provider.Message{Role: provider.RoleUser, Content: "next"})
|
|
sess.Add(provider.Message{Role: provider.RoleAssistant, Content: "ok"})
|
|
// Point archive at a file path so MkdirAll/Create would fail if archives were written.
|
|
badArchive := filepath.Join(testenv.TempDir(t), "not-a-dir")
|
|
if err := writeFile(badArchive, []byte("x")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a := New(fp, nil, sess, Options{
|
|
ContextWindow: 50_000, CompactRatio: 0.85, ArchiveDir: badArchive,
|
|
}, event.Discard)
|
|
if _, err := a.CompactNow(context.Background(), CompactRequest{}); err != nil {
|
|
t.Fatalf("CompactNow with unusable ArchiveDir: %v", err)
|
|
}
|
|
if len(a.sess.win.compactionState.Projection.Messages) == 0 {
|
|
t.Fatal("expected projection despite unusable ArchiveDir")
|
|
}
|
|
}
|
|
|
|
func writeFile(path string, b []byte) error {
|
|
return os.WriteFile(path, b, 0o644)
|
|
}
|
|
|
|
// visibleContext is what the model actually sees: the frozen body spliced with
|
|
// the live canonical tail. Reading Projection.Messages instead would answer for
|
|
// the body alone, which stopped being the whole view once the tail left it.
|
|
func visibleContext(a *Agent) []provider.Message {
|
|
if a == nil {
|
|
return nil
|
|
}
|
|
return a.window().modelVisibleMessages()
|
|
}
|
|
|
|
func hasCompactionSummary(msgs []provider.Message) bool {
|
|
return slices.ContainsFunc(msgs, isCompactionSummary)
|
|
}
|
|
|
|
func joinContents(msgs []provider.Message) string {
|
|
var b strings.Builder
|
|
for _, m := range msgs {
|
|
b.WriteString(m.Content)
|
|
b.WriteByte('\n')
|
|
}
|
|
return b.String()
|
|
}
|