refactor(decision): move snapshot SQLite storage into internal/state/decisionstore

The decision tool package no longer opens SQLite itself; it maps the new
leaf package's typed errors to its domain codes. Schema, retention,
capacity and version/parent semantics are unchanged. Also restores the
Registry comment in control.Options to its original wording.
This commit is contained in:
esengine committed 2026-09-30 01:32:09 -07:00
1 parent 4b9ab779e1
commit 19a5520014
9 files changed
+439 -287

No files matched your search

+7 -7
View File
@@ -299,9 +299,8 @@ type Options struct {
// SubagentGate is the shared gate every headless-only sub-agent surface
// reads; nil disables gating there. The approval-mode setters Update it so
// a runtime switch reaches sub-agents, not only the executor's own gate.
SubagentGate *SharedHeadlessGate
Label string
ModelRef string
SubagentGate *SharedHeadlessGate
Label, ModelRef string
// ModelModes are the optional modes the session's model declares.
ModelModes []config.ModelMode
SystemPrompt string
@@ -325,6 +324,7 @@ type Options struct {
ReadOnlySkillRunner skill.SubagentRunner
SkillProfile skill.ProfileResolver
Hooks *hook.Runner
DecisionReporter DecisionReporter
Memory *memory.Set
Cleanup func()
// Balance reads the active provider's optional wallet endpoint. Nil, or a
@@ -339,10 +339,10 @@ type Options struct {
TaskStore taskmonitor.WriteStore
// WorkspaceLease is the Delivery writer owner shared with the executor.
WorkspaceLease *workspacelease.Owner
// Registry and PluginCtx support session-scoped MCP server registration.
Registry *tool.Registry
DecisionReporter DecisionReporter
PluginCtx context.Context
// Registry is the executor's live tool set, and PluginCtx the session-scoped
// context; both are needed for hot-adding MCP servers via AddMCPServer.
Registry *tool.Registry
PluginCtx context.Context
// MCPDefaultCallTimeout is the global MCP call cap used by hot-connected
// servers when they do not declare a server- or tool-specific override.
MCPDefaultCallTimeout time.Duration
+7
View File
@@ -0,0 +1,7 @@
// Package decisionstore persists immutable decision request snapshots in a
// SQLite file. A snapshot is an opaque byte payload bound to an optional parent
// version; the package knows nothing about its shape. Entries expire after
// TTL, at most Capacity are kept, and every read verifies the stored digest and
// record checksum. Failures are typed sentinels the caller classifies with
// errors.Is.
package decisionstore
+243
View File
@@ -0,0 +1,243 @@
package decisionstore
import (
"context"
"crypto/rand"
"crypto/sha256"
"database/sql"
"encoding/hex"
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"runtime"
"time"
_ "modernc.org/sqlite"
)
const (
TTL = 7 * 24 * time.Hour
Capacity = 256
)
var (
ErrNotFound = errors.New("decisionstore: snapshot not found")
ErrExpired = errors.New("decisionstore: snapshot expired")
ErrCorrupt = errors.New("decisionstore: snapshot corrupt")
ErrCapacity = errors.New("decisionstore: capacity reached")
ErrOpen = errors.New("decisionstore: open failed")
ErrVersionUnsupported = errors.New("decisionstore: unsupported store version")
ErrRead = errors.New("decisionstore: read failed")
ErrWrite = errors.New("decisionstore: write failed")
ErrID = errors.New("decisionstore: id generation failed")
)
// Record is one stored snapshot.
type Record struct {
Payload []byte
Parent string
Expires time.Time
}
// Store is a SQLite-backed snapshot repository at Path.
type Store struct {
Path string
Now func() time.Time
// Valid, when set, must accept a payload for its record to count as intact.
Valid func([]byte) bool
}
type query interface {
QueryRowContext(context.Context, string, ...any) *sql.Row
}
func ValidID(id string) bool {
if len(id) != 32 {
return false
}
for _, c := range id {
if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f') {
return false
}
}
return true
}
func checksum(id, parent string, expires int64, raw []byte) string {
hash := sha256.Sum256(append(fmt.Appendf(nil, "%s\n%s\n%d\n", id, parent, expires), raw...))
return hex.EncodeToString(hash[:])
}
func (s *Store) open(ctx context.Context, writable bool) (*sql.DB, error) {
if writable {
if err := prepareFile(s.Path); err != nil {
return nil, ErrOpen
}
}
abs, err := filepath.Abs(s.Path)
if err != nil {
return nil, ErrOpen
}
slash := filepath.ToSlash(abs)
if runtime.GOOS == "windows" && len(slash) >= 2 && slash[1] == ':' {
slash = "/" + slash
}
u := url.URL{Scheme: "file", Path: slash}
dsn := u.String() + "?_pragma=busy_timeout%285000%29&_pragma=synchronous%28FULL%29"
if !writable {
dsn += "&mode=ro"
}
db, err := sql.Open("sqlite", dsn)
if err != nil {
return nil, ErrOpen
}
db.SetMaxOpenConns(1)
if err := initialize(ctx, db, writable); err != nil {
_ = db.Close()
return nil, err
}
return db, nil
}
func prepareFile(path string) error {
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return err
}
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return err
}
return file.Close()
}
func initialize(ctx context.Context, db *sql.DB, writable bool) error {
var version int
if err := db.QueryRowContext(ctx, "PRAGMA user_version").Scan(&version); err != nil {
return ErrOpen
}
if version != 0 && version != 1 {
return ErrVersionUnsupported
}
if !writable {
if version != 1 {
return ErrVersionUnsupported
}
return nil
}
if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS decision_snapshots (
id TEXT PRIMARY KEY, parent TEXT NOT NULL, expires INTEGER NOT NULL,
request BLOB NOT NULL, digest TEXT NOT NULL, checksum TEXT NOT NULL,
UNIQUE(parent,digest))`); err != nil {
return ErrOpen
}
if _, err := db.ExecContext(ctx, "PRAGMA user_version=1"); err != nil {
return ErrOpen
}
return nil
}
func (s *Store) read(ctx context.Context, q query, id string, now time.Time) (Record, error) {
var parent, digest, sum string
var expires int64
var raw []byte
err := q.QueryRowContext(ctx, `SELECT parent,expires,CASE WHEN length(request)<=262144 THEN request ELSE NULL END,digest,checksum FROM decision_snapshots WHERE id=?`, id).Scan(&parent, &expires, &raw, &digest, &sum)
if errors.Is(err, sql.ErrNoRows) {
return Record{}, ErrNotFound
}
if err != nil {
return Record{}, ErrRead
}
hash := sha256.Sum256(raw)
if len(raw) == 0 || parent != "" && !ValidID(parent) || hex.EncodeToString(hash[:]) != digest || checksum(id, parent, expires, raw) != sum {
return Record{}, ErrCorrupt
}
until := time.Unix(0, expires).UTC()
if !now.Before(until) {
return Record{}, ErrExpired
}
if s.Valid != nil && !s.Valid(raw) {
return Record{}, ErrCorrupt
}
return Record{Payload: raw, Parent: parent, Expires: until}, nil
}
// Load returns the snapshot id without creating the store file.
func (s *Store) Load(ctx context.Context, id string) (Record, error) {
if _, err := os.Stat(s.Path); errors.Is(err, os.ErrNotExist) {
return Record{}, ErrNotFound
}
db, err := s.open(ctx, false)
if err != nil {
return Record{}, err
}
defer db.Close()
return s.read(ctx, db, id, s.Now())
}
// Save stores payload under parent, returning the existing id for an identical
// (parent, payload) pair.
func (s *Store) Save(ctx context.Context, payload []byte, parent string) (string, time.Time, error) {
db, err := s.open(ctx, true)
if err != nil {
return "", time.Time{}, err
}
defer db.Close()
conn, err := db.Conn(ctx)
if err != nil {
return "", time.Time{}, ErrWrite
}
defer conn.Close()
if _, err := conn.ExecContext(ctx, "BEGIN IMMEDIATE"); err != nil {
return "", time.Time{}, ErrWrite
}
defer func() { _, _ = conn.ExecContext(context.Background(), "ROLLBACK") }()
id, expires, err := s.insert(ctx, conn, payload, parent)
if err != nil {
return "", time.Time{}, err
}
if _, err := conn.ExecContext(ctx, "COMMIT"); err != nil {
return "", time.Time{}, ErrWrite
}
return id, expires, nil
}
func (s *Store) insert(ctx context.Context, conn *sql.Conn, raw []byte, parent string) (string, time.Time, error) {
now := s.Now().UTC()
if parent != "" {
if _, err := s.read(ctx, conn, parent, now); err != nil {
return "", time.Time{}, err
}
}
if _, err := conn.ExecContext(ctx, "DELETE FROM decision_snapshots WHERE expires<=?", now.UnixNano()); err != nil {
return "", time.Time{}, ErrWrite
}
digest := sha256.Sum256(raw)
hexdigest := hex.EncodeToString(digest[:])
var existing string
err := conn.QueryRowContext(ctx, "SELECT id FROM decision_snapshots WHERE parent=? AND digest=?", parent, hexdigest).Scan(&existing)
if err == nil {
row, err := s.read(ctx, conn, existing, now)
return existing, row.Expires, err
}
if !errors.Is(err, sql.ErrNoRows) {
return "", time.Time{}, ErrRead
}
var count int
if err := conn.QueryRowContext(ctx, "SELECT count(*) FROM decision_snapshots").Scan(&count); err != nil {
return "", time.Time{}, ErrRead
}
if count >= Capacity {
return "", time.Time{}, ErrCapacity
}
var nonce [16]byte
if _, err := rand.Read(nonce[:]); err != nil {
return "", time.Time{}, ErrID
}
id, expires := hex.EncodeToString(nonce[:]), now.Add(TTL)
if _, err := conn.ExecContext(ctx, "INSERT INTO decision_snapshots VALUES(?,?,?,?,?,?)", id, parent, expires.UnixNano(), raw, hexdigest, checksum(id, parent, expires.UnixNano(), raw)); err != nil {
return "", time.Time{}, ErrWrite
}
return id, expires, nil
}
+121
View File
@@ -0,0 +1,121 @@
package decisionstore
import (
"context"
"errors"
"fmt"
"path/filepath"
"sync"
"testing"
"time"
)
func newStore(t *testing.T, now func() time.Time) *Store {
t.Helper()
return &Store{Path: filepath.Join(t.TempDir(), "state.sqlite"), Now: now}
}
func TestCorruptionDetected(t *testing.T) {
for _, column := range []string{"request", "expires", "parent", "digest", "checksum"} {
t.Run(column, func(t *testing.T) {
s := newStore(t, time.Now)
id, _, err := s.Save(context.Background(), []byte(`{"a":1}`), "")
if err != nil {
t.Fatal(err)
}
db, err := s.open(context.Background(), true)
if err != nil {
t.Fatal(err)
}
_, err = db.Exec("UPDATE decision_snapshots SET "+column+"=? WHERE id=?", "corrupt", id)
_ = db.Close()
if err != nil {
t.Fatal(err)
}
if _, err = s.Load(context.Background(), id); !errors.Is(err, ErrCorrupt) && !errors.Is(err, ErrRead) {
t.Fatalf("corruption not rejected: %v", err)
}
})
}
}
func TestConcurrentDedupAndExpiry(t *testing.T) {
now := time.Now().UTC()
clock := func() time.Time { return now }
s := newStore(t, clock)
payload := []byte(`{"a":1}`)
id, expires, err := s.Save(context.Background(), payload, "")
if err != nil || expires.Sub(now) != TTL {
t.Fatal("incorrect retention")
}
var wg sync.WaitGroup
for range 8 {
wg.Go(func() {
other := &Store{Path: s.Path, Now: clock}
got, until, err := other.Save(context.Background(), payload, "")
if err != nil || got != id || !until.Equal(expires) {
t.Errorf("concurrent retry changed identity: %s %v", got, err)
}
})
}
wg.Wait()
now = expires
if _, err := s.Load(context.Background(), id); !errors.Is(err, ErrExpired) {
t.Fatal("expired snapshot accepted")
}
newID, _, err := s.Save(context.Background(), payload, "")
if err != nil || newID == id {
t.Fatal("expired snapshot not replaced")
}
}
func TestParentLinkAndMissingParent(t *testing.T) {
s := newStore(t, time.Now)
ctx := context.Background()
parent, _, err := s.Save(ctx, []byte(`{"v":1}`), "")
if err != nil {
t.Fatal(err)
}
child, _, err := s.Save(ctx, []byte(`{"v":1}`), parent)
if err != nil || child == parent {
t.Fatalf("same payload under a new parent must be a new version: %v", err)
}
if rec, err := s.Load(ctx, child); err != nil || rec.Parent != parent {
t.Fatalf("parent link lost: %v", err)
}
if _, _, err := s.Save(ctx, []byte(`{"v":2}`), "00000000000000000000000000000000"); !errors.Is(err, ErrNotFound) {
t.Fatalf("missing parent accepted: %v", err)
}
}
func TestLoadDoesNotCreateFile(t *testing.T) {
s := newStore(t, time.Now)
if _, err := s.Load(context.Background(), "00000000000000000000000000000000"); !errors.Is(err, ErrNotFound) {
t.Fatal(err)
}
}
func TestCapacityAndUnsupportedVersion(t *testing.T) {
s := newStore(t, time.Now)
ctx := context.Background()
for i := range Capacity {
if _, _, err := s.Save(ctx, fmt.Appendf(nil, `{"n":%d}`, i), ""); err != nil {
t.Fatal(err)
}
}
if _, _, err := s.Save(ctx, []byte(`{"n":"x"}`), ""); !errors.Is(err, ErrCapacity) {
t.Fatal("capacity ignored")
}
db, err := s.open(ctx, true)
if err != nil {
t.Fatal(err)
}
_, err = db.Exec("PRAGMA user_version=99")
_ = db.Close()
if err != nil {
t.Fatal(err)
}
if _, _, err := s.Save(ctx, []byte(`{"n":"y"}`), ""); !errors.Is(err, ErrVersionUnsupported) {
t.Fatal("unsupported store overwritten")
}
}
-94
View File
@@ -1,94 +0,0 @@
package decision
import (
"context"
"database/sql"
"net/url"
"os"
"path/filepath"
"runtime"
"time"
_ "modernc.org/sqlite"
)
const diskTTL = 7 * 24 * time.Hour
const diskCapacity = 256
type diskRepository struct {
path string
now func() time.Time
}
func (r *diskRepository) persistent() bool { return true }
func (r *diskRepository) open(ctx context.Context, writable bool) (*sql.DB, *problem) {
if writable {
if err := prepareDiskFile(r.path); err != nil {
return nil, err
}
}
abs, err := filepath.Abs(r.path)
if err != nil {
return nil, &problem{Code: "decision.store_open_failed"}
}
slash := filepath.ToSlash(abs)
if runtime.GOOS == "windows" && len(slash) >= 2 && slash[1] == ':' {
slash = "/" + slash
}
u := url.URL{Scheme: "file", Path: slash}
dsn := u.String() + "?_pragma=busy_timeout%285000%29&_pragma=synchronous%28FULL%29"
if !writable {
dsn += "&mode=ro"
}
db, err := sql.Open("sqlite", dsn)
if err != nil {
return nil, &problem{Code: "decision.store_open_failed"}
}
db.SetMaxOpenConns(1)
if invalid := initializeDisk(ctx, db, writable); invalid != nil {
_ = db.Close()
return nil, invalid
}
return db, nil
}
func prepareDiskFile(path string) *problem {
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return &problem{Code: "decision.store_open_failed"}
}
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return &problem{Code: "decision.store_open_failed"}
}
if err := file.Close(); err != nil {
return &problem{Code: "decision.store_open_failed"}
}
return nil
}
func initializeDisk(ctx context.Context, db *sql.DB, writable bool) *problem {
var version int
if err := db.QueryRowContext(ctx, "PRAGMA user_version").Scan(&version); err != nil {
return &problem{Code: "decision.store_open_failed"}
}
if version != 0 && version != 1 {
return &problem{Code: "decision.store_version_unsupported"}
}
if !writable {
if version != 1 {
return &problem{Code: "decision.store_version_unsupported"}
}
return nil
}
if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS decision_snapshots (
id TEXT PRIMARY KEY, parent TEXT NOT NULL, expires INTEGER NOT NULL,
request BLOB NOT NULL, digest TEXT NOT NULL, checksum TEXT NOT NULL,
UNIQUE(parent,digest))`); err != nil {
return &problem{Code: "decision.store_open_failed"}
}
if _, err := db.ExecContext(ctx, "PRAGMA user_version=1"); err != nil {
return &problem{Code: "decision.store_open_failed"}
}
return nil
}
+45 -98
View File
@@ -2,62 +2,66 @@ package decision
import (
"context"
"crypto/rand"
"crypto/sha256"
"database/sql"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"time"
"reasonix/internal/state/decisionstore"
)
type diskQuery interface {
QueryRowContext(context.Context, string, ...any) *sql.Row
type diskRepository struct {
store *decisionstore.Store
}
func recordChecksum(id, parent string, expires int64, raw []byte) string {
hash := sha256.Sum256(append(fmt.Appendf(nil, "%s\n%s\n%d\n", id, parent, expires), raw...))
return hex.EncodeToString(hash[:])
func newDiskRepository(path string, now func() time.Time) *diskRepository {
return &diskRepository{store: &decisionstore.Store{
Path: path,
Now: now,
Valid: func(raw []byte) bool {
_, invalid := decode(raw)
return invalid == nil
},
}}
}
func readDisk(ctx context.Context, query diskQuery, id string, now time.Time) (savedRequest, *problem) {
var parent, digest, checksum string
var expires int64
var raw []byte
err := query.QueryRowContext(ctx, `SELECT parent,expires,CASE WHEN length(request)<=262144 THEN request ELSE NULL END,digest,checksum FROM decision_snapshots WHERE id=?`, id).Scan(&parent, &expires, &raw, &digest, &checksum)
if errors.Is(err, sql.ErrNoRows) {
return savedRequest{}, &problem{Code: "decision.snapshot_not_found", ID: id}
func (r *diskRepository) persistent() bool { return true }
func storeProblem(err error, id string) *problem {
codes := []struct {
err error
code string
}{
{decisionstore.ErrNotFound, "decision.snapshot_not_found"},
{decisionstore.ErrExpired, "decision.snapshot_expired"},
{decisionstore.ErrCorrupt, "decision.snapshot_corrupt"},
{decisionstore.ErrRead, "decision.store_read_failed"},
{decisionstore.ErrCapacity, "decision.snapshot_capacity"},
{decisionstore.ErrOpen, "decision.store_open_failed"},
{decisionstore.ErrVersionUnsupported, "decision.store_version_unsupported"},
{decisionstore.ErrID, "decision.snapshot_id_failed"},
}
if err != nil {
return savedRequest{}, &problem{Code: "decision.store_read_failed", ID: id}
for _, c := range codes {
if errors.Is(err, c.err) {
out := &problem{Code: c.code}
if errors.Is(err, decisionstore.ErrNotFound) || errors.Is(err, decisionstore.ErrExpired) || errors.Is(err, decisionstore.ErrCorrupt) || errors.Is(err, decisionstore.ErrRead) {
out.ID = id
}
return out
}
}
hash := sha256.Sum256(raw)
if len(raw) == 0 || parent != "" && !validSnapshotID(parent) || hex.EncodeToString(hash[:]) != digest || recordChecksum(id, parent, expires, raw) != checksum {
return savedRequest{}, &problem{Code: "decision.snapshot_corrupt", ID: id}
}
until := time.Unix(0, expires).UTC()
if !now.Before(until) {
return savedRequest{}, &problem{Code: "decision.snapshot_expired", ID: id}
}
req, invalid := decode(raw)
if invalid != nil {
return savedRequest{}, &problem{Code: "decision.snapshot_corrupt", ID: id}
}
return savedRequest{req, until, parent}, nil
return &problem{Code: "decision.store_write_failed"}
}
func (r *diskRepository) load(ctx context.Context, id string) (savedRequest, *problem) {
if _, err := os.Stat(r.path); errors.Is(err, os.ErrNotExist) {
return savedRequest{}, &problem{Code: "decision.snapshot_not_found", ID: id}
rec, err := r.store.Load(ctx, id)
if err != nil {
return savedRequest{}, storeProblem(err, id)
}
db, invalid := r.open(ctx, false)
req, invalid := decode(rec.Payload)
if invalid != nil {
return savedRequest{}, invalid
return savedRequest{}, &problem{Code: "decision.snapshot_corrupt", ID: id}
}
defer db.Close()
return readDisk(ctx, db, id, r.now())
return savedRequest{req, rec.Expires, rec.Parent}, nil
}
func (r *diskRepository) save(ctx context.Context, req request, parent string) (string, time.Time, *problem) {
@@ -68,66 +72,9 @@ func (r *diskRepository) save(ctx context.Context, req request, parent string) (
if len(raw) > 256*1024 {
return "", time.Time{}, &problem{Code: "decision.snapshot_input_limit"}
}
db, invalid := r.open(ctx, true)
if invalid != nil {
return "", time.Time{}, invalid
}
defer db.Close()
conn, err := db.Conn(ctx)
id, expires, err := r.store.Save(ctx, raw, parent)
if err != nil {
return "", time.Time{}, &problem{Code: "decision.store_write_failed"}
}
defer conn.Close()
if _, err := conn.ExecContext(ctx, "BEGIN IMMEDIATE"); err != nil {
return "", time.Time{}, &problem{Code: "decision.store_write_failed"}
}
defer func() { _, _ = conn.ExecContext(context.Background(), "ROLLBACK") }()
id, expires, invalid := r.insert(ctx, conn, raw, parent)
if invalid != nil {
return "", time.Time{}, invalid
}
if _, err := conn.ExecContext(ctx, "COMMIT"); err != nil {
return "", time.Time{}, &problem{Code: "decision.store_write_failed"}
}
return id, expires, nil
}
func (r *diskRepository) insert(ctx context.Context, conn *sql.Conn, raw []byte, parent string) (string, time.Time, *problem) {
now := r.now().UTC()
if parent != "" {
if _, invalid := readDisk(ctx, conn, parent, now); invalid != nil {
return "", time.Time{}, invalid
}
}
if _, err := conn.ExecContext(ctx, "DELETE FROM decision_snapshots WHERE expires<=?", now.UnixNano()); err != nil {
return "", time.Time{}, &problem{Code: "decision.store_write_failed"}
}
digest := sha256.Sum256(raw)
hexdigest := hex.EncodeToString(digest[:])
var existing string
err := conn.QueryRowContext(ctx, "SELECT id FROM decision_snapshots WHERE parent=? AND digest=?", parent, hexdigest).Scan(&existing)
if err == nil {
row, invalid := readDisk(ctx, conn, existing, now)
return existing, row.Expires, invalid
}
if !errors.Is(err, sql.ErrNoRows) {
return "", time.Time{}, &problem{Code: "decision.store_read_failed"}
}
var count int
if err := conn.QueryRowContext(ctx, "SELECT count(*) FROM decision_snapshots").Scan(&count); err != nil {
return "", time.Time{}, &problem{Code: "decision.store_read_failed"}
}
if count >= diskCapacity {
return "", time.Time{}, &problem{Code: "decision.snapshot_capacity"}
}
var nonce [16]byte
if _, err := rand.Read(nonce[:]); err != nil {
return "", time.Time{}, &problem{Code: "decision.snapshot_id_failed"}
}
id, expires := hex.EncodeToString(nonce[:]), now.Add(diskTTL)
_, err = conn.ExecContext(ctx, "INSERT INTO decision_snapshots VALUES(?,?,?,?,?,?)", id, parent, expires.UnixNano(), raw, hexdigest, recordChecksum(id, parent, expires.UnixNano(), raw))
if err != nil {
return "", time.Time{}, &problem{Code: "decision.store_write_failed"}
return "", time.Time{}, storeProblem(err, parent)
}
return id, expires, nil
}
+12 -76
View File
@@ -3,11 +3,9 @@ package decision
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"sync"
"testing"
"time"
)
@@ -57,7 +55,7 @@ func TestPersistentRestartAndEvidenceVersions(t *testing.T) {
t.Fatalf("version binding lost: %+v", got)
}
}
repo := &diskRepository{path: path, now: time.Now}
repo := newDiskRepository(path, time.Now)
old, invalid := repo.load(context.Background(), original.SnapshotID)
if invalid != nil || !reflect.DeepEqual(old.Request, fixture()) {
t.Fatal("old request changed")
@@ -85,60 +83,6 @@ func TestPersistentFailureAndIsolation(t *testing.T) {
assertSnapshotError(t, runtimeResult(t, tools[0], fixture()), "decision.store_open_failed")
}
func TestPersistentCorruptionDetected(t *testing.T) {
for _, column := range []string{"request", "expires", "parent", "digest", "checksum"} {
t.Run(column, func(t *testing.T) {
repo := &diskRepository{path: filepath.Join(t.TempDir(), "state.sqlite"), now: time.Now}
id, _, invalid := repo.save(context.Background(), fixture(), "")
if invalid != nil {
t.Fatal(invalid)
}
db, invalid := repo.open(context.Background(), true)
if invalid != nil {
t.Fatal(invalid)
}
_, err := db.Exec("UPDATE decision_snapshots SET "+column+"=? WHERE id=?", "corrupt", id)
_ = db.Close()
if err != nil {
t.Fatal(err)
}
_, invalid = repo.load(context.Background(), id)
if invalid == nil || (invalid.Code != "decision.snapshot_corrupt" && invalid.Code != "decision.store_read_failed") {
t.Fatalf("corruption not rejected: %v", invalid)
}
})
}
}
func TestPersistentConcurrentDedupAndExpiry(t *testing.T) {
path := filepath.Join(t.TempDir(), "state.sqlite")
now := time.Now().UTC()
repo := &diskRepository{path: path, now: func() time.Time { return now }}
id, expires, invalid := repo.save(context.Background(), fixture(), "")
if invalid != nil || expires.Sub(now) != diskTTL {
t.Fatal("incorrect retention")
}
var wg sync.WaitGroup
for range 8 {
wg.Go(func() {
other := &diskRepository{path: path, now: func() time.Time { return now }}
got, until, invalid := other.save(context.Background(), fixture(), "")
if invalid != nil || got != id || !until.Equal(expires) {
t.Errorf("concurrent retry changed identity: %s %v", got, invalid)
}
})
}
wg.Wait()
now = expires
if _, invalid := repo.load(context.Background(), id); invalid == nil || invalid.Code != "decision.snapshot_expired" {
t.Fatal("expired snapshot accepted")
}
newID, _, invalid := repo.save(context.Background(), fixture(), "")
if invalid != nil || newID == id {
t.Fatal("expired snapshot not replaced")
}
}
func TestEvidenceUpdateRejectsImplicitReplacement(t *testing.T) {
tools := NewRuntime()
original := decodeResult(t, runtimeResult(t, tools[0], fixture()))
@@ -159,29 +103,21 @@ func TestEvidenceUpdateRejectsImplicitReplacement(t *testing.T) {
}
}
func TestPersistentCapacityAndUnsupportedVersion(t *testing.T) {
repo := &diskRepository{path: filepath.Join(t.TempDir(), "state.sqlite"), now: time.Now}
func TestPersistentStoreErrorsKeepDomainCodes(t *testing.T) {
repo := newDiskRepository(filepath.Join(t.TempDir(), "state.sqlite"), time.Now)
ctx := context.Background()
for i := range diskCapacity {
req := fixture()
req.Options[0].Name = fmt.Sprintf("option-%d", i)
if _, _, invalid := repo.save(ctx, req, ""); invalid != nil {
t.Fatal(invalid)
}
}
if _, _, invalid := repo.save(ctx, fixture(), ""); invalid == nil || invalid.Code != "decision.snapshot_capacity" {
t.Fatal("capacity ignored")
}
db, invalid := repo.open(ctx, true)
id, _, invalid := repo.save(ctx, fixture(), "")
if invalid != nil {
t.Fatal(invalid)
}
_, err := db.Exec("PRAGMA user_version=99")
_ = db.Close()
if err != nil {
t.Fatal(err)
missing := "00000000000000000000000000000000"
if _, invalid := repo.load(ctx, missing); invalid == nil || invalid.Code != "decision.snapshot_not_found" || invalid.ID != missing {
t.Fatalf("missing: %v", invalid)
}
if _, _, invalid := repo.save(ctx, fixture(), ""); invalid == nil || invalid.Code != "decision.store_version_unsupported" {
t.Fatal("unsupported store overwritten")
if _, _, invalid := repo.save(ctx, fixture(), missing); invalid == nil || invalid.Code != "decision.snapshot_not_found" || invalid.ID != missing {
t.Fatalf("missing parent: %v", invalid)
}
if got, invalid := repo.load(ctx, id); invalid != nil || !reflect.DeepEqual(got.Request, fixture()) {
t.Fatal("round trip changed request")
}
}
+3 -12
View File
@@ -6,6 +6,7 @@ import (
"time"
"reasonix/internal/contract/tool"
"reasonix/internal/state/decisionstore"
)
type runtimeCompare struct {
@@ -27,7 +28,7 @@ func NewRuntime() []tool.Tool {
// NewPersistentRuntime uses a host-selected database path without memory fallback.
func NewPersistentRuntime(path string) []tool.Tool {
store := &diskRepository{path: path, now: time.Now}
store := newDiskRepository(path, time.Now)
return runtimeTools(store)
}
@@ -99,14 +100,4 @@ func (t runtimeCheck) Execute(ctx context.Context, args json.RawMessage) (string
return string(data), err
}
func validSnapshotID(id string) bool {
if len(id) != 32 {
return false
}
for _, character := range id {
if !(character >= '0' && character <= '9' || character >= 'a' && character <= 'f') {
return false
}
}
return true
}
func validSnapshotID(id string) bool { return decisionstore.ValidID(id) }
+1
View File
@@ -31,6 +31,7 @@ var leaves = []string{
"internal/platform/computer",
"internal/base/diff",
"internal/ext/extension/rpcwire",
"internal/state/decisionstore",
"internal/state/execgraph",
"internal/state/execjournal",
"internal/ext/extensioncontract",