f99010fae1
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
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
320 lines
11 KiB
Go
320 lines
11 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestExecWithoutCancelDropsTempTableWithCanceledContext(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "test.db")
|
|
pool, err := sql.Open("sqlite3", path)
|
|
require.NoError(t, err, "open sqlite")
|
|
defer pool.Close()
|
|
|
|
baseCtx := context.Background()
|
|
conn, err := pool.Conn(baseCtx)
|
|
require.NoError(t, err, "pin sqlite connection")
|
|
defer conn.Close()
|
|
|
|
_, err = conn.ExecContext(baseCtx, `
|
|
CREATE TEMP TABLE _test_cleanup (
|
|
id TEXT PRIMARY KEY
|
|
)`)
|
|
require.NoError(t, err, "create temp table")
|
|
|
|
ctx, cancel := context.WithCancel(baseCtx)
|
|
cancel()
|
|
|
|
_, err = execWithoutCancel(ctx, conn,
|
|
"DROP TABLE IF EXISTS _test_cleanup")
|
|
require.NoError(t, err, "drop with canceled context")
|
|
|
|
_, err = conn.ExecContext(baseCtx, `
|
|
CREATE TEMP TABLE _test_cleanup (
|
|
id TEXT PRIMARY KEY
|
|
)`)
|
|
require.NoError(t, err, "recreate temp table after cleanup")
|
|
}
|
|
|
|
func TestCopyOrphanedDataSanitizesCopiedContent(t *testing.T) {
|
|
ctx := context.Background()
|
|
dir := t.TempDir()
|
|
srcPath := filepath.Join(dir, "old.db")
|
|
srcDB := testDBAtPath(t, srcPath, "src")
|
|
insertSession(t, srcDB, "poison-orphan", "proj")
|
|
insertMessages(t, srcDB, userMsg("poison-orphan", 0, "clean"))
|
|
var messageID int64
|
|
require.NoError(t, srcDB.getWriter().QueryRowContext(ctx,
|
|
`SELECT id FROM messages WHERE session_id = ? AND ordinal = 0`,
|
|
"poison-orphan",
|
|
).Scan(&messageID), "query source message id")
|
|
|
|
messageContent := "message\x00body\x01\nkept"
|
|
toolInput := "{\"cmd\":\"tool\x00input\x04\"}"
|
|
emptyToolInput := "\x00\x04"
|
|
toolResult := "tool\x00result\x02"
|
|
emptyToolResult := "\x00\x04"
|
|
eventContent := "event\x00content\x03"
|
|
const (
|
|
messageLengthExcess = 7
|
|
toolLengthExcess = 11
|
|
eventLengthExcess = 5
|
|
emptyResultLength = 7
|
|
)
|
|
_, err := srcDB.getWriter().ExecContext(ctx,
|
|
`UPDATE messages
|
|
SET content = ?, content_length = ?
|
|
WHERE id = ?`,
|
|
messageContent, len(messageContent)+messageLengthExcess, messageID,
|
|
)
|
|
require.NoError(t, err, "plant poisoned message")
|
|
_, err = srcDB.getWriter().ExecContext(ctx,
|
|
`INSERT INTO tool_calls (
|
|
message_id, session_id, tool_name, category,
|
|
tool_use_id, input_json, result_content_length,
|
|
result_content, call_index
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
messageID, "poison-orphan", "Read", "file", "tool-1",
|
|
toolInput, len(toolResult)+toolLengthExcess, toolResult, 0,
|
|
)
|
|
require.NoError(t, err, "plant poisoned tool call")
|
|
_, err = srcDB.getWriter().ExecContext(ctx,
|
|
`INSERT INTO tool_calls (
|
|
message_id, session_id, tool_name, category,
|
|
tool_use_id, input_json, call_index
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
messageID, "poison-orphan", "Read", "file", "tool-empty",
|
|
emptyToolInput, 1,
|
|
)
|
|
require.NoError(t, err, "plant empty-sanitized tool input")
|
|
_, err = srcDB.getWriter().ExecContext(ctx,
|
|
`INSERT INTO tool_calls (
|
|
message_id, session_id, tool_name, category,
|
|
tool_use_id, result_content_length, result_content,
|
|
call_index
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
messageID, "poison-orphan", "Read", "file", "tool-empty-result",
|
|
emptyResultLength, emptyToolResult, 2,
|
|
)
|
|
require.NoError(t, err, "plant empty-sanitized tool result")
|
|
_, err = srcDB.getWriter().ExecContext(ctx,
|
|
`INSERT INTO tool_result_events (
|
|
session_id, tool_call_message_ordinal, call_index,
|
|
tool_use_id, source, status, content, content_length,
|
|
event_index
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
"poison-orphan", 0, 0, "tool-1", "tool_result", "ok",
|
|
eventContent, len(eventContent)+eventLengthExcess, 0,
|
|
)
|
|
require.NoError(t, err, "plant poisoned tool result event")
|
|
// Dirty content only exists in archives written before
|
|
// sanitizedSourceDataVersion; sources at or above it skip the
|
|
// sanitize pass entirely.
|
|
_, err = srcDB.getWriter().ExecContext(ctx, fmt.Sprintf(
|
|
"PRAGMA user_version = %d", sanitizedSourceDataVersion-1,
|
|
))
|
|
require.NoError(t, err, "downgrade source data version")
|
|
require.NoError(t, srcDB.Close(), "close source")
|
|
|
|
dstPath := filepath.Join(dir, "new.db")
|
|
dstDB := testDBAtPath(t, dstPath, "dst")
|
|
defer dstDB.Close()
|
|
|
|
count, err := dstDB.CopyOrphanedDataFrom(srcPath)
|
|
require.NoError(t, err, "CopyOrphanedDataFrom")
|
|
require.Equal(t, 1, count, "expected one orphan")
|
|
|
|
var gotMessage string
|
|
var gotMessageLength int
|
|
require.NoError(t, dstDB.getReader().QueryRowContext(ctx,
|
|
`SELECT content, content_length
|
|
FROM messages
|
|
WHERE session_id = ? AND ordinal = 0`,
|
|
"poison-orphan",
|
|
).Scan(&gotMessage, &gotMessageLength), "query copied message")
|
|
wantMessage := SanitizeUTF8(messageContent)
|
|
assert.Equal(t, wantMessage, gotMessage)
|
|
assert.Equal(t, len(wantMessage)+messageLengthExcess, gotMessageLength)
|
|
|
|
var gotToolInput string
|
|
require.NoError(t, dstDB.getReader().QueryRowContext(ctx,
|
|
`SELECT input_json
|
|
FROM tool_calls
|
|
WHERE session_id = ? AND call_index = 0`,
|
|
"poison-orphan",
|
|
).Scan(&gotToolInput), "query copied tool input")
|
|
assert.Equal(t, SanitizeUTF8(toolInput), gotToolInput)
|
|
|
|
var gotEmptyToolInput sql.NullString
|
|
require.NoError(t, dstDB.getReader().QueryRowContext(ctx,
|
|
`SELECT input_json
|
|
FROM tool_calls
|
|
WHERE session_id = ? AND call_index = 1`,
|
|
"poison-orphan",
|
|
).Scan(&gotEmptyToolInput), "query empty copied tool input")
|
|
assert.False(t, gotEmptyToolInput.Valid)
|
|
|
|
var gotToolResult string
|
|
var gotToolResultLength int
|
|
require.NoError(t, dstDB.getReader().QueryRowContext(ctx,
|
|
`SELECT result_content, result_content_length
|
|
FROM tool_calls
|
|
WHERE session_id = ? AND call_index = 0`,
|
|
"poison-orphan",
|
|
).Scan(&gotToolResult, &gotToolResultLength), "query copied tool call")
|
|
wantToolResult := SanitizeUTF8(toolResult)
|
|
assert.Equal(t, wantToolResult, gotToolResult)
|
|
assert.Equal(t, len(wantToolResult)+toolLengthExcess, gotToolResultLength)
|
|
|
|
var gotEmptyToolResult sql.NullString
|
|
var gotEmptyToolResultLength sql.NullInt64
|
|
require.NoError(t, dstDB.getReader().QueryRowContext(ctx,
|
|
`SELECT result_content, result_content_length
|
|
FROM tool_calls
|
|
WHERE session_id = ? AND call_index = 2`,
|
|
"poison-orphan",
|
|
).Scan(
|
|
&gotEmptyToolResult,
|
|
&gotEmptyToolResultLength,
|
|
), "query empty copied tool call result")
|
|
assert.False(t, gotEmptyToolResult.Valid)
|
|
require.True(t, gotEmptyToolResultLength.Valid)
|
|
assert.Equal(t,
|
|
int64(emptyResultLength-len(emptyToolResult)),
|
|
gotEmptyToolResultLength.Int64,
|
|
)
|
|
|
|
var gotEventContent string
|
|
var gotEventLength int
|
|
require.NoError(t, dstDB.getReader().QueryRowContext(ctx,
|
|
`SELECT content, content_length
|
|
FROM tool_result_events
|
|
WHERE session_id = ? AND event_index = 0`,
|
|
"poison-orphan",
|
|
).Scan(&gotEventContent, &gotEventLength), "query copied tool result event")
|
|
wantEventContent := SanitizeUTF8(eventContent)
|
|
assert.Equal(t, wantEventContent, gotEventContent)
|
|
assert.Equal(t, len(wantEventContent)+eventLengthExcess, gotEventLength)
|
|
}
|
|
|
|
// TestCopySkipsSanitizeForSanitizedSource guards the resync fast
|
|
// path: each sanitize pass is skipped once the source data version
|
|
// proves ingest already sanitized that field — content/results at
|
|
// sanitizedSourceDataVersion, input_json at the later
|
|
// sanitizedInputSourceDataVersion — and skipped rows survive the
|
|
// copy verbatim.
|
|
func TestCopySkipsSanitizeForSanitizedSource(t *testing.T) {
|
|
const rawContent = "nul\x00byte\x01kept"
|
|
const rawToolInput = "{\"cmd\":\"a\x00b\x01\"}"
|
|
const rawToolResult = "result\x00kept\x01"
|
|
copies := []struct {
|
|
name string
|
|
trash bool
|
|
copy func(dst *DB, srcPath string) (int, error)
|
|
}{
|
|
{
|
|
name: "orphaned",
|
|
copy: func(dst *DB, srcPath string) (int, error) {
|
|
return dst.CopyOrphanedDataFrom(srcPath)
|
|
},
|
|
},
|
|
{
|
|
name: "trashed",
|
|
trash: true,
|
|
copy: func(dst *DB, srcPath string) (int, error) {
|
|
return dst.CopyTrashedDataFrom(srcPath)
|
|
},
|
|
},
|
|
}
|
|
versions := []struct {
|
|
name string
|
|
sourceVersion int
|
|
wantInput string
|
|
}{
|
|
{
|
|
// Ingest at v58 sanitized content and results but not
|
|
// input_json, so only the input pass runs for it.
|
|
name: "content-sanitized source pays input pass",
|
|
sourceVersion: sanitizedSourceDataVersion,
|
|
wantInput: SanitizeUTF8(rawToolInput),
|
|
},
|
|
{
|
|
// A source at the input watermark is fully clean; every
|
|
// pass is skipped and rows copy verbatim.
|
|
name: "fully sanitized source copies verbatim",
|
|
sourceVersion: sanitizedInputSourceDataVersion,
|
|
wantInput: rawToolInput,
|
|
},
|
|
}
|
|
for _, cp := range copies {
|
|
for _, ver := range versions {
|
|
t.Run(cp.name+"/"+ver.name, func(t *testing.T) {
|
|
ctx := context.Background()
|
|
dir := t.TempDir()
|
|
srcPath := filepath.Join(dir, "old.db")
|
|
srcDB := testDBAtPath(t, srcPath, "src")
|
|
insertSession(t, srcDB, "sess", "proj")
|
|
insertMessages(t, srcDB, userMsg("sess", 0, "clean"))
|
|
_, err := srcDB.getWriter().ExecContext(ctx,
|
|
`UPDATE messages SET content = ? WHERE session_id = ?`,
|
|
rawContent, "sess",
|
|
)
|
|
require.NoError(t, err, "plant raw content")
|
|
var messageID int64
|
|
require.NoError(t, srcDB.getWriter().QueryRowContext(ctx,
|
|
`SELECT id FROM messages WHERE session_id = ?`, "sess",
|
|
).Scan(&messageID), "read message id")
|
|
_, err = srcDB.getWriter().ExecContext(ctx,
|
|
`INSERT INTO tool_calls (
|
|
message_id, session_id, tool_name, category,
|
|
tool_use_id, input_json, result_content_length,
|
|
result_content, call_index
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
messageID, "sess", "Bash", "execution", "tool-1",
|
|
rawToolInput, len(rawToolResult), rawToolResult, 0,
|
|
)
|
|
require.NoError(t, err, "plant raw tool call")
|
|
_, err = srcDB.getWriter().ExecContext(ctx, fmt.Sprintf(
|
|
"PRAGMA user_version = %d", ver.sourceVersion,
|
|
))
|
|
require.NoError(t, err, "set source data version")
|
|
if cp.trash {
|
|
require.NoError(t, srcDB.SoftDeleteSession("sess"),
|
|
"soft delete source session")
|
|
}
|
|
require.NoError(t, srcDB.Close(), "close source")
|
|
|
|
dstDB := testDBAtPath(t, filepath.Join(dir, "new.db"), "dst")
|
|
defer dstDB.Close()
|
|
count, err := cp.copy(dstDB, srcPath)
|
|
require.NoError(t, err, "copy from source")
|
|
require.Equal(t, 1, count, "copied sessions")
|
|
|
|
var got string
|
|
require.NoError(t, dstDB.getReader().QueryRowContext(ctx,
|
|
`SELECT content FROM messages WHERE session_id = ?`,
|
|
"sess",
|
|
).Scan(&got), "query copied message")
|
|
assert.Equal(t, rawContent, got,
|
|
"content-sanitized source must copy content verbatim")
|
|
|
|
var gotInput, gotResult string
|
|
require.NoError(t, dstDB.getReader().QueryRowContext(ctx,
|
|
`SELECT input_json, result_content
|
|
FROM tool_calls WHERE session_id = ?`,
|
|
"sess",
|
|
).Scan(&gotInput, &gotResult), "query copied tool call")
|
|
assert.Equal(t, ver.wantInput, gotInput)
|
|
assert.Equal(t, rawToolResult, gotResult,
|
|
"tool result must copy verbatim for sanitized sources")
|
|
})
|
|
}
|
|
}
|
|
}
|