Files
kenn-io--agentsview/internal/parser/sqlite_container_state_test.go
wehub-resource-sync f99010fae1
Desktop Artifacts / Desktop Build (Linux) (push) Waiting to run
Desktop Artifacts / Desktop Build (Windows) (push) Waiting to run
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Waiting to run
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

183 lines
7.3 KiB
Go

package parser
import (
"encoding/binary"
"os"
"runtime"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestSQLiteContainerStateIgnoresSubSecondMtimeChanges pins the state's
// timestamp contract: mtime participates at whole-second granularity only,
// so sub-second timestamp instability (filesystems and stat round-trips
// disagree below one second) can never make an unchanged container look
// changed, while a change that crosses a second boundary still registers.
// Content changes are detected by SQLite's own write markers regardless of
// any timestamp.
func TestSQLiteContainerStateIgnoresSubSecondMtimeChanges(t *testing.T) {
dbPath, _, db := newTestDB(t)
require.NoError(t, db.Close())
base := time.Unix(1700000000, 100*int64(time.Millisecond))
require.NoError(t, os.Chtimes(dbPath, base, base))
before, ok := StatSQLiteContainerState(dbPath)
require.True(t, ok, "state must be readable")
nudged := time.Unix(1700000000, 900*int64(time.Millisecond))
require.NoError(t, os.Chtimes(dbPath, nudged, nudged))
within, ok := StatSQLiteContainerState(dbPath)
require.True(t, ok, "state must be readable after sub-second nudge")
assert.Equal(t, before, within,
"a sub-second mtime change must not change the container state")
crossed := time.Unix(1700000001, 0)
require.NoError(t, os.Chtimes(dbPath, crossed, crossed))
afterSecond, ok := StatSQLiteContainerState(dbPath)
require.True(t, ok, "state must be readable after second boundary")
assert.NotEqual(t, before, afterSecond,
"an mtime change across a second boundary must change the state")
}
// TestSQLiteContainerStateRejectsNonSQLiteFiles pins that a file without a
// SQLite header can never be captured, and therefore never trusted as an
// unchanged container.
func TestSQLiteContainerStateRejectsNonSQLiteFiles(t *testing.T) {
path := t.TempDir() + "/opencode.db"
require.NoError(t, os.WriteFile(
path, []byte("not a sqlite database, padded to header size........................................................."), 0o644,
))
_, ok := StatSQLiteContainerState(path)
assert.False(t, ok, "non-SQLite bytes must not produce a container state")
}
// TestSQLiteContainerStateDetectsFileReplacement pins that swapping the
// container for a different file is visible even when every header marker
// and the stat metadata coincide: a byte-identical copy at a new inode with
// a restored mtime must still change the state. Without file identity, a
// restored or replaced database landing in the same second with the same
// size and change counter would be indistinguishable from the original.
func TestSQLiteContainerStateDetectsFileReplacement(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("file identity is unavailable on Windows")
}
dbPath, _, db := newTestDB(t)
require.NoError(t, db.Close())
before, ok := StatSQLiteContainerState(dbPath)
require.True(t, ok, "state must be readable before replacement")
raw, err := os.ReadFile(dbPath)
require.NoError(t, err, "read container bytes")
info, err := os.Stat(dbPath)
require.NoError(t, err, "stat container")
// Write the copy while the original still exists so the two files are
// guaranteed distinct inodes, then rename over the original. A
// remove-then-recreate at the same path can reuse the freed inode and
// make the replacement genuinely indistinguishable.
replacement := dbPath + ".replacement"
require.NoError(t, os.WriteFile(replacement, raw, 0o644),
"write replacement container with identical bytes")
require.NoError(t, os.Rename(replacement, dbPath),
"swap replacement over container")
require.NoError(t,
os.Chtimes(dbPath, info.ModTime(), info.ModTime()),
"restore container mtime")
after, ok := StatSQLiteContainerState(dbPath)
require.True(t, ok, "state must be readable after replacement")
assert.NotEqual(t, before, after,
"a replaced container file must change the state")
}
// TestSQLiteContainerStateRejectsUnknownWALFormat pins that the WAL fields
// are only trusted when the WAL header carries the documented magic and
// format version (3007000). The salts and checkpoint sequence are what make
// the equality check sound; under any other format those bytes are opaque,
// so the container must fail closed to "never trusted" instead.
func TestSQLiteContainerStateRejectsUnknownWALFormat(t *testing.T) {
writeWAL := func(t *testing.T, dbPath string, magic, version uint32) {
t.Helper()
header := make([]byte, 40)
binary.BigEndian.PutUint32(header[0:4], magic)
binary.BigEndian.PutUint32(header[4:8], version)
binary.BigEndian.PutUint32(header[12:16], 7) // checkpoint seq
binary.BigEndian.PutUint32(header[16:20], 11) // salt-1
binary.BigEndian.PutUint32(header[20:24], 13) // salt-2
require.NoError(t, os.WriteFile(dbPath+"-wal", header, 0o644))
}
t.Run("documented header is read", func(t *testing.T) {
dbPath, _, db := newTestDB(t)
require.NoError(t, db.Close())
writeWAL(t, dbPath, 0x377f0683, 3007000)
state, ok := StatSQLiteContainerState(dbPath)
require.True(t, ok, "documented WAL header must be readable")
assert.Equal(t, uint32(7), state.WALCkptSeq)
assert.Equal(t, uint32(11), state.WALSalt1)
assert.Equal(t, uint32(13), state.WALSalt2)
})
t.Run("wrong magic fails closed", func(t *testing.T) {
dbPath, _, db := newTestDB(t)
require.NoError(t, db.Close())
writeWAL(t, dbPath, 0xdeadbeef, 3007000)
_, ok := StatSQLiteContainerState(dbPath)
assert.False(t, ok,
"a WAL without the documented magic must never be trusted")
})
t.Run("unknown version fails closed", func(t *testing.T) {
dbPath, _, db := newTestDB(t)
require.NoError(t, db.Close())
writeWAL(t, dbPath, 0x377f0682, 3008000)
_, ok := StatSQLiteContainerState(dbPath)
assert.False(t, ok,
"an unknown WAL format version must never be trusted")
})
}
// TestOpenCodeProjectsCacheReusesUntilContainerChanges pins the
// project-table cache used by SQLite session parsing: while the container
// state is unchanged the previous load is served back, and any committed
// write invalidates it. Without the cache every parsed session re-queried
// the full project table, which dominated re-parse CPU on large archives.
func TestOpenCodeProjectsCacheReusesUntilContainerChanges(t *testing.T) {
dbPath, seeder, db := newTestDB(t)
defer db.Close()
seeder.AddProject("prj_1", "/home/user/code/app-one")
first, err := loadOpenCodeProjectsCached(db, dbPath)
require.NoError(t, err)
assert.Equal(t,
map[string]string{"prj_1": "/home/user/code/app-one"}, first)
// Poison the cached copy to make hits observable: an unchanged
// container must serve the poisoned entry back, and a changed one must
// reload the real values from the DB.
openCodeProjectsCacheMu.Lock()
entry := openCodeProjectsCache[dbPath]
entry.projects = map[string]string{"prj_1": "cached-marker"}
openCodeProjectsCache[dbPath] = entry
openCodeProjectsCacheMu.Unlock()
second, err := loadOpenCodeProjectsCached(db, dbPath)
require.NoError(t, err)
assert.Equal(t, "cached-marker", second["prj_1"],
"an unchanged container must be served from the cache")
_, err = db.Exec(
"UPDATE project SET worktree = ? WHERE id = ?",
"/home/user/code/renamed", "prj_1",
)
require.NoError(t, err)
third, err := loadOpenCodeProjectsCached(db, dbPath)
require.NoError(t, err)
assert.Equal(t, "/home/user/code/renamed", third["prj_1"],
"a committed write must invalidate the cached projects")
}