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
183 lines
7.3 KiB
Go
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")
|
|
}
|