Files
DeepSeek-Reasonix/internal/agent/session_checkpoint_lineage_test.go
T
yhh b18d8ef3aa feat(studio): storage relocation, composer intake, and the checkpoint agreement
Accumulated work from the branch, carried across the rebase onto origin/studio
and merged with what landed there in the meantime.

Storage: a workspace can name where its state lives, and the server tracks the
one relocation it may be running (internal/storage, config/storage_roots,
serve/storage, the Storage panel).

Composer intake: what a drop or a long paste becomes is one pure function —
payload in, action out — so "what happens if I drop a folder" is a test rather
than a branch of an onDrop handler. It sits on the drop router that landed
upstream: that one answers which pane a drop hit, this one answers what it
means and says so before the pointer is released. A paste long enough to bury
the composer becomes a chip beside it instead.

Session: the checkpoint agreement and the lineage/torn-log recovery cases,
plus the conflict diagnostic learning where two transcripts parted and which
roles stood there — a position and a role tell a local reshape from a foreign
write without recording any content.

Model refs: the default-model write checks the live catalog, so an extension
model the runtime knows can be persisted as the default.
2026-08-19 23:52:04 -07:00

87 lines
2.9 KiB
Go

package agent
import (
"os"
"testing"
"reasonix/internal/provider"
"reasonix/internal/store"
)
// The checkpoint is what a reader falls back to when the log cannot answer, so
// the two must never describe different transcripts. An append the read model
// declines used to leave it on the previous save's history: a second lineage
// beside the log, waiting for the first fallback to pick it.
func TestDeclinedReadModelAppendStillLeavesOneLineage(t *testing.T) {
dir := t.TempDir()
path := dir + "/session.jsonl"
live := NewSession("sys")
live.Add(provider.Message{Role: provider.RoleUser, Content: "第一句话", RawContent: "第一句话"})
live.Add(provider.Message{Role: provider.RoleAssistant, Content: "hi"})
if err := live.SaveSnapshot(path); err != nil {
t.Fatal(err)
}
// The index a save is allowed to lose (it is warn-only there) is exactly
// what the in-place append needs to prove the checkpoint is the log's
// prefix, so losing it is how the decline happens in the field.
if err := os.Remove(store.SessionDisplayIndex(path)); err != nil {
t.Fatalf("drop display index: %v", err)
}
live.Add(provider.Message{Role: provider.RoleUser, Content: "第二句话", RawContent: "第二句话"})
if err := live.SaveSnapshot(path); err != nil {
t.Fatal(err)
}
logged, _, damaged, err := loadSessionMessages(path)
if err != nil || damaged {
t.Fatalf("replay = %v damaged=%v", err, damaged)
}
checkpoint, err := loadSessionMessagesFromJSONL(path)
if err != nil {
t.Fatal(err)
}
if !messagesEqualForStorageList(logged, checkpoint) {
t.Fatalf("checkpoint %q disagrees with the log %q",
sessionUserTexts(checkpoint), sessionUserTexts(logged))
}
if got := sessionUserTexts(checkpoint); len(got) != 2 {
t.Fatalf("checkpoint users = %q, want both turns", got)
}
}
// Republishing the read model also republishes the index that proves it, so a
// session that lost one recovers the in-place append instead of paying a whole
// rewrite on every later save.
func TestRepublishedReadModelRearmsTheAppendPath(t *testing.T) {
dir := t.TempDir()
path := dir + "/session.jsonl"
live := NewSession("sys")
live.Add(provider.Message{Role: provider.RoleUser, Content: "第一句话", RawContent: "第一句话"})
if err := live.SaveSnapshot(path); err != nil {
t.Fatal(err)
}
if err := os.Remove(store.SessionDisplayIndex(path)); err != nil {
t.Fatal(err)
}
live.Add(provider.Message{Role: provider.RoleAssistant, Content: "hi"})
if err := live.SaveSnapshot(path); err != nil {
t.Fatal(err)
}
idx, err := LoadSessionDisplayIndex(store.SessionDisplayIndex(path))
if err != nil {
t.Fatalf("index was not republished: %v", err)
}
if idx.MessageCount != 3 {
t.Fatalf("index message count = %d, want 3", idx.MessageCount)
}
info, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
if idx.TranscriptSize != info.Size() {
t.Fatalf("index describes %d bytes of a %d-byte checkpoint", idx.TranscriptSize, info.Size())
}
}